Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.4.1-upgrade-release
  • 1121-download
  • 1218-smartplaylist_backend
  • 1373-login-form-move-reset-your-password-link
  • 1381-progress-bars
  • 1481
  • 1518-update-django-allauth
  • 1645
  • 1675-widget-improperly-configured-missing-resource-id
  • 1675-widget-improperly-configured-missing-resource-id-2
  • 1704-required-props-are-not-always-passed
  • 1716-add-frontend-tests-again
  • 1749-smtp-uri-configuration
  • 1930-first-upload-in-a-batch-always-fails
  • 1976-update-documentation-links-in-readme-files
  • 2054-player-layout
  • 2063-funkwhale-connection-interrupted-every-so-often-requires-network-reset-page-refresh
  • 2091-iii-6-improve-visuals-layout
  • 2151-refused-to-load-spa-manifest-json-2
  • 2154-add-to-playlist-pop-up-hidden-by-now-playing-screen
  • 2155-can-t-see-the-episode-list-of-a-podcast-as-an-anonymous-user-with-anonymous-access-enabled
  • 2156-add-management-command-to-change-file-ref-for-in-place-imported-files-to-s3
  • 2192-clear-queue-bug-when-random-shuffle-is-enabled
  • 2205-channel-page-pagination-link-dont-working
  • 2215-custom-logger-does-not-work-at-all-with-webkit-and-blink-based-browsers
  • 2228-troi-real-world-review
  • 2274-implement-new-upload-api
  • 2303-allow-users-to-own-tagged-items
  • 2395-far-right-filter
  • 2405-front-buttont-trigger-third-party-hook
  • 2408-troi-create-missing-tracks
  • 2416-revert-library-drop
  • 2448-complete-tags
  • 2451-delete-no-user-query
  • 2452-fetch-third-party-metadata
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • activitypub-overview
  • album-sliders
  • arne/2091-improve-visuals
  • back-option-for-edits
  • chore/2406-compose-modularity-scope
  • develop
  • develop-password-reset
  • env-file-cleanup
  • feat/2091-improve-visuals
  • fix-amd64-docker-build-gfortran
  • fix-gitpod
  • fix-plugins-dev-setup
  • fix-rate-limit-serializer
  • fix-schema-channel-metadata-choices
  • flupsi/2803-improve-visuals
  • flupsi/2804-new-upload-process
  • funkwhale-fix_pwa_manifest
  • funkwhale-petitminion-2136-bug-fix-prune-skipped-upload
  • funkwhale-ui-buttons
  • georg/add-typescript
  • gitpod/test-1866
  • global-button-experiment
  • global-buttons
  • juniorjpdj/pkg-repo
  • manage-py-reference
  • merge-review
  • minimal-python-version
  • petitminion-develop-patch-84496
  • pin-mutagen-to-1.46
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • pre-release/1.3.0
  • prune_skipped_uploads_docs
  • renovate/front-all-dependencies
  • renovate/front-major-all-dependencies
  • schema-updates
  • small-gitpod-improvements
  • spectacular_schema
  • stable
  • tempArne
  • ui-buttons
  • update-frontend-dependencies
  • upload-process-spec
  • user-concept-docs
  • v2-artists
  • vite-ws-ssl-compatible
  • wip/2091-improve-visuals
  • wvffle/dependency-maintenance
  • wvffle/new-upload-process
  • wvffle/ui-rewrite-sidebar
  • wvffle/ui-sidebar
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
  • 1.2.10
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.6-1
  • 1.2.7
  • 1.2.8
  • 1.2.9
  • 1.3.0
  • 1.3.0-rc1
  • 1.3.0-rc2
  • 1.3.0-rc3
  • 1.3.0-rc4
  • 1.3.0-rc5
  • 1.3.0-rc6
  • 1.3.1
  • 1.3.2
  • 1.3.3
  • 1.3.4
  • 1.4.0
  • 1.4.0-rc1
  • 1.4.0-rc2
  • 1.4.1
  • 2.0.0-alpha.1
  • 2.0.0-alpha.2
190 results

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
  • 1121-download
  • 1218-smartplaylist_backend
  • 1288-user-me-can-be-created-but-cannot-be-edited
  • 1381-progress-bars
  • 1434-update-pyld
  • 1481
  • 1515-update-click
  • 1518-update-django-allauth
  • 1645
  • 1659-following-rss-feed-is-broken
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • Keunes-stable-patch-11152
  • Keunes-stable-patch-91132
  • album-sliders
  • back-option-for-edits
  • develop
  • generate-swagger
  • master
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • renovate/configure
  • stable
  • update-boto3
  • update-frontend-dependencies
  • update-uvicorn
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
100 results
Show changes
Showing
with 387 additions and 137 deletions
......@@ -3,12 +3,9 @@ import urllib.parse
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from rest_framework import serializers
from funkwhale_api.common import preferences
from funkwhale_api.common import middleware
from funkwhale_api.common import utils
from funkwhale_api.common import middleware, preferences, utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import spa_views
......@@ -64,7 +61,7 @@ def channel_detail(query, redirect_to_ap):
"rel": "alternate",
"type": "application/rss+xml",
"href": obj.get_rss_url(),
"title": "{} - RSS Podcast Feed".format(obj.artist.name),
"title": f"{obj.artist.name} - RSS Podcast Feed",
},
)
......@@ -76,7 +73,7 @@ def channel_detail(query, redirect_to_ap):
"type": "application/json+oembed",
"href": (
utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed"))
+ "?format=json&url={}".format(urllib.parse.quote_plus(obj_url))
+ f"?format=json&url={urllib.parse.quote_plus(obj_url)}"
),
}
)
......
......@@ -7,8 +7,7 @@ from django.utils import timezone
from funkwhale_api.taskapp import celery
from . import models
from . import serializers
from . import models, serializers
logger = logging.getLogger(__name__)
......
from rest_framework import decorators
from rest_framework import exceptions
from rest_framework import mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response
from rest_framework import viewsets
from django import http
from django.db import transaction
from django.db.models import Count, Prefetch, Q, Sum
from django.utils import timezone
from drf_spectacular.utils import extend_schema, extend_schema_view, inline_serializer
from rest_framework import decorators, exceptions, mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response
from rest_framework import serializers as rest_serializers
from rest_framework import viewsets
from funkwhale_api.common import locales
from funkwhale_api.common import permissions
from funkwhale_api.common import preferences
from funkwhale_api.common import locales, permissions, preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.federation import actors
......@@ -27,19 +24,28 @@ from funkwhale_api.users.oauth import permissions as oauth_permissions
from . import categories, filters, models, renderers, serializers
ARTIST_PREFETCH_QS = (
music_models.Artist.objects.select_related("description", "attachment_cover",)
music_models.Artist.objects.select_related(
"description",
"attachment_cover",
)
.prefetch_related(music_views.TAG_PREFETCH)
.annotate(_tracks_count=Count("tracks"))
.annotate(_tracks_count=Count("artist_credit__tracks"))
)
class ChannelsMixin(object):
class ChannelsMixin:
def dispatch(self, request, *args, **kwargs):
if not preferences.get("audio__channels_enabled"):
return http.HttpResponse(status=405)
return super().dispatch(request, *args, **kwargs)
@extend_schema_view(
metedata_choices=extend_schema(operation_id="get_channel_metadata_choices"),
subscribe=extend_schema(operation_id="subscribe_channel"),
unsubscribe=extend_schema(operation_id="unsubscribe_channel"),
rss_subscribe=extend_schema(operation_id="subscribe_channel_rss"),
)
class ChannelViewSet(
ChannelsMixin,
MultipleLookupDetailMixin,
......@@ -91,13 +97,15 @@ class ChannelViewSet(
return serializers.ChannelSerializer
elif self.action in ["update", "partial_update"]:
return serializers.ChannelUpdateSerializer
elif self.action == "create":
return serializers.ChannelCreateSerializer
return serializers.ChannelSerializer
def get_queryset(self):
queryset = super().get_queryset()
if self.action == "retrieve":
queryset = queryset.annotate(
_downloads_count=Sum("artist__tracks__downloads_count")
_downloads_count=Sum("artist__artist_credit__tracks__downloads_count")
)
return queryset
......@@ -131,6 +139,7 @@ class ChannelViewSet(
detail=True,
methods=["post"],
permission_classes=[rest_permissions.IsAuthenticated],
serializer_class=serializers.SubscriptionSerializer,
)
def subscribe(self, request, *args, **kwargs):
object = self.get_object()
......@@ -153,6 +162,7 @@ class ChannelViewSet(
data = serializers.SubscriptionSerializer(subscription).data
return response.Response(data, status=201)
@extend_schema(responses={204: None})
@decorators.action(
detail=True,
methods=["post", "delete"],
......@@ -184,7 +194,6 @@ class ChannelViewSet(
if object.attributed_to == actors.get_service_actor():
# external feed, we redirect to the canonical one
return http.HttpResponseRedirect(object.rss_url)
uploads = (
object.library.uploads.playable_by(None)
.prefetch_related(
......@@ -192,7 +201,9 @@ class ChannelViewSet(
"track",
queryset=music_models.Track.objects.select_related(
"attachment_cover", "description"
).prefetch_related(music_views.TAG_PREFETCH,),
).prefetch_related(
music_views.TAG_PREFETCH,
),
),
)
.select_related("track__attachment_cover", "track__description")
......@@ -201,6 +212,32 @@ class ChannelViewSet(
data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads)
return response.Response(data, status=200)
@extend_schema(
responses=inline_serializer(
name="MetedataChoicesSerializer",
fields={
"language": rest_serializers.ListField(
child=inline_serializer(
name="LanguageItem",
fields={
"value": rest_serializers.CharField(),
"label": rest_serializers.CharField(),
},
)
),
"itunes_category": rest_serializers.ListField(
child=inline_serializer(
name="iTunesCategoryItem",
fields={
"value": rest_serializers.CharField(),
"label": rest_serializers.CharField(),
"children": rest_serializers.CharField(),
},
)
),
},
)
)
@decorators.action(
methods=["get"],
detail=False,
......@@ -232,7 +269,9 @@ class ChannelViewSet(
if not serializer.is_valid():
return response.Response(serializer.errors, status=400)
channel = (
models.Channel.objects.filter(rss_url=serializer.validated_data["url"],)
models.Channel.objects.filter(
rss_url=serializer.validated_data["url"],
)
.order_by("id")
.first()
)
......@@ -243,7 +282,10 @@ class ChannelViewSet(
serializer.validated_data["url"]
)
except serializers.FeedFetchException as e:
return response.Response({"detail": str(e)}, status=400,)
return response.Response(
{"detail": str(e)},
status=400,
)
subscription = federation_models.Follow(actor=request.user.actor)
subscription.fid = subscription.get_federation_id()
......@@ -312,6 +354,10 @@ class SubscriptionsViewSet(
qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor)
@extend_schema(
responses=serializers.AllSubscriptionsSerializer(),
operation_id="get_all_subscriptions",
)
@decorators.action(methods=["get"], detail=False)
def all(self, request, *args, **kwargs):
"""
......@@ -319,12 +365,7 @@ class SubscriptionsViewSet(
to have a performant endpoint and avoid lots of queries just to display
subscription status in the UI
"""
subscriptions = list(
self.get_queryset().values_list("uuid", "target__channel__uuid")
)
subscriptions = self.get_queryset().values("uuid", "target__channel__uuid")
payload = {
"results": [{"uuid": str(u[0]), "channel": u[1]} for u in subscriptions],
"count": len(subscriptions),
}
payload = serializers.AllSubscriptionsSerializer(subscriptions).data
return response.Response(payload, status=200)
import click
import functools
import click
@click.group()
def cli():
......
......@@ -6,7 +6,8 @@ from . import base
def handler_add_tags_from_tracks(
artists=False, albums=False,
artists=False,
albums=False,
):
result = None
if artists:
......@@ -19,7 +20,7 @@ def handler_add_tags_from_tracks(
if result is None:
click.echo(" No relevant tags found")
else:
click.echo(" Relevant tags added to {} objects".format(len(result)))
click.echo(f" Relevant tags added to {len(result)} objects")
@base.cli.group()
......
import click
import sys
from . import base
import click
from rest_framework.exceptions import ValidationError
from . import library # noqa
from . import media # noqa
from . import plugins # noqa
from . import users # noqa
from rest_framework.exceptions import ValidationError
from . import base
def invoke():
......@@ -16,7 +16,7 @@ def invoke():
except ValidationError as e:
click.secho("Invalid data:", fg="red")
for field, errors in e.detail.items():
click.secho(" {}:".format(field), fg="red")
click.secho(f" {field}:", fg="red")
for error in errors:
click.secho(" - {}".format(error), fg="red")
click.secho(f" - {error}", fg="red")
sys.exit(1)
import click
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
from versatileimagefield import settings as vif_settings
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.models import Attachment
......@@ -41,19 +39,15 @@ def generate_thumbnails(delete):
(Attachment, "file", "attachment_square"),
]
for model, attribute, key_set in MODELS:
click.echo(
"Generating thumbnails for {}.{}…".format(model._meta.label, attribute)
)
qs = model.objects.exclude(**{"{}__isnull".format(attribute): True})
click.echo(f"Generating thumbnails for {model._meta.label}.{attribute}")
qs = model.objects.exclude(**{f"{attribute}__isnull": True})
qs = qs.exclude(**{attribute: ""})
cache_key = "*{}{}*".format(
settings.MEDIA_URL, vif_settings.VERSATILEIMAGEFIELD_SIZED_DIRNAME
)
entries = cache.keys(cache_key)
if entries:
click.echo(
" Clearing {} cache entries: {}…".format(len(entries), cache_key)
)
click.echo(f" Clearing {len(entries)} cache entries: {cache_key}")
for keys in common_utils.batch(iter(entries)):
cache.delete_many(keys)
warmer = VersatileImageFieldWarmer(
......@@ -64,6 +58,4 @@ def generate_thumbnails(delete):
)
click.echo(" Creating images")
num_created, failed_to_create = warmer.warm()
click.echo(
" {} created, {} in error".format(num_created, len(failed_to_create))
)
click.echo(f" {num_created} created, {len(failed_to_create)} in error")
......@@ -5,7 +5,6 @@ import sys
import click
from django.conf import settings
from . import base
......
import click
from django.db import transaction
from funkwhale_api.federation import models as federation_models
from funkwhale_api.users import models
from funkwhale_api.users import serializers
from funkwhale_api.users import tasks
from funkwhale_api.users import models, serializers, tasks
from . import base
from . import utils
from . import base, utils
class FakeRequest(object):
class FakeRequest:
def __init__(self, session={}):
self.session = session
......@@ -42,17 +38,18 @@ def handler_create_user(
utils.logger.debug("Creating user…")
user = serializer.save(request=request)
utils.logger.debug("Setting permissions and other attributes…")
user.is_staff = is_staff
user.is_staff = is_staff or is_superuser # Always set staff if superuser is set
user.upload_quota = upload_quota
user.is_superuser = is_superuser
for permission in permissions:
if permission in models.PERMISSIONS:
utils.logger.debug("Setting %s permission to True", permission)
setattr(user, "permission_{}".format(permission), True)
setattr(user, f"permission_{permission}", True)
else:
utils.logger.warn("Unknown permission %s", permission)
utils.logger.debug("Creating actor…")
user.actor = models.create_actor(user)
models.create_user_libraries(user)
user.save()
return user
......@@ -60,7 +57,7 @@ def handler_create_user(
@transaction.atomic
def handler_delete_user(usernames, soft=True):
for username in usernames:
click.echo("Deleting {}…".format(username))
click.echo(f"Deleting {username}")
actor = None
user = None
try:
......@@ -157,13 +154,16 @@ def users():
type=click.INT,
)
@click.option(
"--superuser/--no-superuser", default=False,
"--superuser/--no-superuser",
default=False,
)
@click.option(
"--staff/--no-staff", default=False,
"--staff/--no-staff",
default=False,
)
@click.option(
"--permission", multiple=True,
"--permission",
multiple=True,
)
def create(username, password, email, superuser, staff, permission, upload_quota):
"""Create a new user"""
......@@ -179,9 +179,9 @@ def create(username, password, email, superuser, staff, permission, upload_quota
permissions=permission,
upload_quota=upload_quota,
)
click.echo("User {} created!".format(user.username))
click.echo(f"User {user.username} created!")
if generated_password:
click.echo(" Generated password: {}".format(generated_password))
click.echo(f" Generated password: {generated_password}")
@base.delete_command(group=users, id_var="username")
......@@ -210,7 +210,9 @@ def delete(username, hard):
@click.option("--permission-settings/--no-permission-settings", default=None)
@click.option("--password", default=None, envvar="FUNKWHALE_CLI_USER_UPDATE_PASSWORD")
@click.option(
"-q", "--upload-quota", type=click.INT,
"-q",
"--upload-quota",
type=click.INT,
)
def update(username, **kwargs):
"""Update attributes for given users"""
......
from django.contrib.admin import register as initial_register, site, ModelAdmin # noqa
from django.contrib.admin import site # noqa: F401
from django.contrib.admin import ModelAdmin
from django.contrib.admin import register as initial_register
from django.db.models.fields.related import RelatedField
from . import models
from . import tasks
from . import models, tasks
def register(model):
......
......@@ -3,8 +3,7 @@ from django.conf import settings
from config import plugins
from . import mutations
from . import utils
from . import mutations, utils
class CommonConfig(AppConfig):
......
from django.conf import settings
from django.utils.translation import ugettext as _
from allauth.account.models import EmailAddress
from django.core.cache import cache
from allauth.account.utils import send_email_confirmation
from django.utils.translation import gettext as _
from oauth2_provider.contrib.rest_framework.authentication import (
OAuth2Authentication as BaseOAuth2Authentication,
)
......@@ -12,14 +9,6 @@ from rest_framework import exceptions
from funkwhale_api.users import models as users_models
def should_verify_email(user):
if user.is_superuser:
return False
has_unverified_email = not user.has_verified_primary_email
mandatory_verification = settings.ACCOUNT_EMAIL_VERIFICATION != "optional"
return has_unverified_email and mandatory_verification
class UnverifiedEmail(Exception):
def __init__(self, user):
self.user = user
......@@ -27,13 +16,17 @@ class UnverifiedEmail(Exception):
def resend_confirmation_email(request, user):
THROTTLE_DELAY = 500
cache_key = "auth:resent-email-confirmation:{}".format(user.pk)
cache_key = f"auth:resent-email-confirmation:{user.pk}"
if cache.get(cache_key):
return False
done = send_email_confirmation(request, user)
# We do the sending of the conformation by hand because we don't want to pass the request down
# to the email rendering, which would cause another UnverifiedEmail Exception and restarts the sending
# again and again
email = EmailAddress.objects.get_for_user(user, user.email)
email.send_confirmation()
cache.set(cache_key, True, THROTTLE_DELAY)
return done
return True
class OAuth2Authentication(BaseOAuth2Authentication):
......@@ -45,7 +38,7 @@ class OAuth2Authentication(BaseOAuth2Authentication):
resend_confirmation_email(request, e.user)
class ApplicationTokenAuthentication(object):
class ApplicationTokenAuthentication:
def authenticate(self, request):
try:
header = request.headers["Authorization"]
......@@ -68,7 +61,7 @@ class ApplicationTokenAuthentication(object):
msg = _("User account is disabled.")
raise exceptions.AuthenticationFailed(msg)
if should_verify_email(user):
if user.should_verify_email():
raise UnverifiedEmail(user)
request.scopes = application.scope.split()
......
......@@ -19,6 +19,10 @@ class JsonAuthConsumer(JsonWebsocketConsumer):
channels.group_add(group, self.channel_name)
def disconnect(self, close_code):
if self.scope.get("user", False) and self.scope.get("user").pk is not None:
groups = self.scope["user"].get_channels_groups() + self.groups
else:
groups = self.groups
for group in groups:
channels.group_discard(group, self.channel_name)
from django.db import transaction
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import decorators, exceptions, response, status
from rest_framework import decorators
from rest_framework import exceptions
from rest_framework import response
from rest_framework import status
from . import filters
from . import models
from . import filters, models
from . import mutations as common_mutations
from . import serializers
from . import signals
from . import tasks
from . import utils
from . import serializers, signals, tasks, utils
def action_route(serializer_class):
......@@ -87,6 +80,16 @@ def mutations_route(types):
)
return response.Response(serializer.data, status=status.HTTP_201_CREATED)
return decorators.action(
return extend_schema(
methods=["post"], responses=serializers.APIMutationSerializer()
)(
extend_schema(
methods=["get"],
responses=serializers.APIMutationSerializer(many=True),
parameters=[OpenApiParameter("id", location="query", exclude=True)],
)(
decorators.action(
methods=["get", "post"], detail=True, required_scope="edits"
)(mutations)
)
)
import factory
from funkwhale_api.factories import registry, NoUpdateOnCreate
from funkwhale_api.factories import NoUpdateOnCreate, registry
from funkwhale_api.federation import factories as federation_factories
......
......@@ -2,7 +2,6 @@ import django_filters
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from rest_framework import serializers
from . import search
......@@ -25,9 +24,22 @@ def privacy_level_query(user, lookup_field="privacy_level", user_field="user"):
if user.is_anonymous:
return models.Q(**{lookup_field: "everyone"})
return models.Q(
**{"{}__in".format(lookup_field): ["instance", "everyone"]}
) | models.Q(**{lookup_field: "me", user_field: user})
followers_query = models.Q(
**{
f"{lookup_field}": "followers",
f"{user_field}__actor__in": user.actor.get_approved_followings(),
}
)
# Federated TrackFavorite don't have an user associated with the trackfavorite.actor
# to do : if we implement the followers privacy_level this will become a problem
no_user_query = models.Q(**{f"{user_field}__isnull": True})
return (
models.Q(**{f"{lookup_field}__in": ["instance", "everyone"]})
| models.Q(**{lookup_field: "me", user_field: user})
| followers_query
| no_user_query
)
class SearchFilter(django_filters.CharFilter):
......@@ -58,7 +70,7 @@ class SmartSearchFilter(django_filters.CharFilter):
return qs
try:
cleaned = self.config.clean(value)
except (forms.ValidationError):
except forms.ValidationError:
return qs.none()
return search.apply(qs, cleaned)
......@@ -98,7 +110,7 @@ def get_generic_filter_query(value, relation_name, choices):
obj = related_queryset.get(obj_query)
except related_queryset.model.DoesNotExist:
raise forms.ValidationError("Invalid object")
filter_query &= models.Q(**{"{}_id".format(relation_name): obj.id})
filter_query &= models.Q(**{f"{relation_name}_id": obj.id})
return filter_query
......@@ -164,7 +176,7 @@ class GenericRelation(serializers.JSONField):
id_value = v[id_attr]
id_value = id_field.to_internal_value(id_value)
except (TypeError, KeyError, serializers.ValidationError):
raise serializers.ValidationError("Invalid {}".format(id_attr))
raise serializers.ValidationError(f"Invalid {id_attr}")
query_getter = conf.get(
"get_query", lambda attr, value: models.Q(**{attr: value})
......
from django import forms
from django.db.models import Q
from django_filters import widgets
from django.db.models.functions import Lower
from django_filters import rest_framework as filters
from django_filters import widgets
from drf_spectacular.utils import extend_schema_field
from . import fields
from . import models
from . import search
from . import utils
from . import fields, models, search, utils
class NoneObject(object):
class NoneObject:
def __eq__(self, other):
return other.__class__ == NoneObject
......@@ -49,9 +47,10 @@ class CoerceChoiceField(forms.ChoiceField):
try:
return [b for a, b in self.choices if v == a][0]
except IndexError:
raise forms.ValidationError("Invalid value {}".format(value))
raise forms.ValidationError(f"Invalid value {value}")
@extend_schema_field(bool)
class NullBooleanFilter(filters.ChoiceFilter):
field_class = CoerceChoiceField
......@@ -65,9 +64,7 @@ class NullBooleanFilter(filters.ChoiceFilter):
return qs
if value == NONE:
value = None
qs = self.get_method(qs)(
**{"%s__%s" % (self.field_name, self.lookup_expr): value}
)
qs = self.get_method(qs)(**{f"{self.field_name}__{self.lookup_expr}": value})
return qs.distinct() if self.distinct else qs
......@@ -123,7 +120,6 @@ class MultipleQueryFilter(filters.TypedMultipleChoiceFilter):
def filter_target(value):
config = {
"artist": ["artist", "target_id", int],
"album": ["album", "target_id", int],
......@@ -181,7 +177,6 @@ class ActorScopeFilter(filters.CharFilter):
super().__init__(*args, **kwargs)
def filter(self, queryset, value):
if not value:
return queryset
......@@ -219,7 +214,7 @@ class ActorScopeFilter(filters.CharFilter):
if not self.library_field:
predicate = "pk__in"
else:
predicate = "{}__in".format(self.library_field)
predicate = f"{self.library_field}__in"
return Q(**{predicate: followed_libraries})
elif scope.startswith("actor:"):
......@@ -227,7 +222,8 @@ class ActorScopeFilter(filters.CharFilter):
username, domain = full_username.split("@")
try:
actor = federation_models.Actor.objects.get(
preferred_username__iexact=username, domain_id=domain,
preferred_username__iexact=username,
domain_id=domain,
)
except federation_models.Actor.DoesNotExist:
raise EmptyQuerySet()
......@@ -235,7 +231,7 @@ class ActorScopeFilter(filters.CharFilter):
return Q(**{self.actor_field: actor})
elif scope.startswith("domain:"):
domain = scope.split("domain:", 1)[1]
return Q(**{"{}__domain_id".format(self.actor_field): domain})
return Q(**{f"{self.actor_field}__domain_id": domain})
else:
raise EmptyQuerySet()
......@@ -244,3 +240,19 @@ class ActorScopeFilter(filters.CharFilter):
raise EmptyQuerySet()
return Q(**{self.actor_field: actor})
class CaseInsensitiveNameOrderingFilter(filters.OrderingFilter):
def filter(self, qs, value):
order_by = []
if value is None:
return qs
for param in value:
if param == "name":
order_by.append(Lower("name"))
else:
order_by.append(self.get_ordering_value(param))
return qs.order_by(*order_by)
from django.conf import settings
from django.contrib.auth.management.commands.createsuperuser import (
Command as BaseCommand,
)
from django.core.management.base import CommandError
class Command(BaseCommand):
def handle(self, *apps_label, **options):
"""
Creating Django Superusers would bypass some of our username checks, which can lead to unexpected behaviour.
We therefore prohibit the execution of the command.
"""
force = settings.FORCE
if not force == 1:
raise CommandError(
"Running createsuperuser on your Funkwhale instance bypasses some of our checks "
"which can lead to unexpected behavior of your instance. We therefore suggest to "
"run `funkwhale-manage fw users create --superuser` instead."
)
return super().handle(*apps_label, **options)
import os
import debugpy
import uvicorn
from django.core.management import call_command
from django.core.management.commands.migrate import Command as BaseCommand
from funkwhale_api.common import preferences
from funkwhale_api.music.models import Library
from funkwhale_api.users.models import User
class Command(BaseCommand):
help = "Manage gitpod environment"
def add_arguments(self, parser):
parser.add_argument("command", nargs="?", type=str)
def handle(self, *args, **options):
command = options["command"]
if not command:
return self.show_help()
if command == "init":
return self.init()
if command == "dev":
return self.dev()
def show_help(self):
self.stdout.write("")
self.stdout.write("Available commands:")
self.stdout.write("init - Initialize gitpod workspace")
self.stdout.write("dev - Run Funkwhale in development mode with debug server")
self.stdout.write("")
def init(self):
user = User.objects.get(username="gitpod")
# Allow anonymous access
preferences.set("common__api_authentication_required", False)
# Download music catalog
os.system(
"git clone https://dev.funkwhale.audio/funkwhale/catalog.git /tmp/catalog"
)
os.system("mv -f /tmp/catalog/music /workspace/funkwhale/data")
os.system("rm -rf /tmp/catalog/music")
# Import music catalog into library
call_command(
"create_library",
"gitpod",
name="funkwhale/catalog",
privacy_level="everyone",
)
call_command(
"import_files",
Library.objects.get(actor=user.actor).uuid,
"/workspace/funkwhale/data/music/",
recursive=True,
in_place=True,
no_input=False,
)
def dev(self):
debugpy.listen(5678)
uvicorn.run(
"config.asgi:application",
host="0.0.0.0",
port=5000,
reload=True,
reload_dirs=[
"/workspace/funkwhale/api/config/",
"/workspace/funkwhale/api/funkwhale_api/",
],
)
import pathlib
from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from django.db import transaction
from funkwhale_api.music import models
class Command(BaseCommand):
help = """
Update the reference for Uploads that have been imported with --in-place and are now moved to s3.
Please note: This does not move any file! Make sure you already moved the files to your s3 bucket.
Specify --source to filter the reference to update to files from a specific in-place directory. If no
--source is given, all in-place imported track references will be updated.
Specify --target to specify a subdirectory in the S3 bucket where you moved the files. If no --target is
given, the file is expected to be stored in the same path as before.
Examples:
Music File: /music/Artist/Album/track.ogg
--source: /music
--target unset
All files imported from /music will be updated and expected to be in the same folder structure in the bucket
Music File: /music/Artist/Album/track.ogg
--source: /music
--target: /in_place
The music file is expected to be stored in the bucket in the directory /in_place/Artist/Album/track.ogg
"""
def create_parser(self, *args, **kwargs):
parser = super().create_parser(*args, **kwargs)
parser.formatter_class = RawTextHelpFormatter
return parser
def add_arguments(self, parser):
parser.add_argument(
"--no-dry-run",
action="store_false",
dest="dry_run",
default=True,
help="Disable dry run mode and apply updates for real on the database",
)
parser.add_argument(
"--source",
type=pathlib.Path,
required=True,
help="Specify the path of the directory where the files originally were stored to update their reference.",
)
parser.add_argument(
"--target",
type=pathlib.Path,
help="Specify a subdirectory in the S3 bucket where you moved the files to.",
)
@transaction.atomic
def handle(self, *args, **options):
if options["dry_run"]:
self.stdout.write("Dry-run on, will not touch the database")
else:
self.stdout.write("Dry-run off, *changing the database*")
self.stdout.write("")
prefix = f"file://{options['source']}"
to_change = models.Upload.objects.filter(source__startswith=prefix)
self.stdout.write(f"Found {to_change.count()} uploads to update.")
target = options.get("target")
if target is None:
target = options["source"]
for upl in to_change:
upl.audio_file = str(upl.source).replace(str(prefix), str(target))
upl.source = None
self.stdout.write(f"Upload expected in {upl.audio_file}")
if not options["dry_run"]:
upl.save()
self.stdout.write("")
if options["dry_run"]:
self.stdout.write(
"Nothing was updated, rerun this command with --no-dry-run to apply the changes"
)
else:
self.stdout.write("Updating completed!")
self.stdout.write("")