kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Update Black & run for whole repository
							rodzic
							
								
									68210d5330
								
							
						
					
					
						commit
						850dc69091
					
				| 
						 | 
				
			
			@ -102,12 +102,13 @@ black:
 | 
			
		|||
  variables:
 | 
			
		||||
    GIT_STRATEGY: fetch
 | 
			
		||||
  before_script:
 | 
			
		||||
    - pip install black==19.10b0
 | 
			
		||||
    - pip install black
 | 
			
		||||
  script:
 | 
			
		||||
    - black --check --diff api/
 | 
			
		||||
    - black --check --diff .
 | 
			
		||||
  only: 
 | 
			
		||||
    changes:
 | 
			
		||||
      - api/**/*
 | 
			
		||||
      - "**/*.py"
 | 
			
		||||
      - .gitlab-ci.yml
 | 
			
		||||
 | 
			
		||||
flake8:
 | 
			
		||||
  interruptible: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,10 @@ v1_patterns += [
 | 
			
		|||
        r"^history/",
 | 
			
		||||
        include(("funkwhale_api.history.urls", "history"), namespace="history"),
 | 
			
		||||
    ),
 | 
			
		||||
    url(r"^", include(("funkwhale_api.users.api_urls", "users"), namespace="users"),),
 | 
			
		||||
    url(
 | 
			
		||||
        r"^",
 | 
			
		||||
        include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
 | 
			
		||||
    ),
 | 
			
		||||
    # XXX: remove if Funkwhale 1.1
 | 
			
		||||
    url(
 | 
			
		||||
        r"^users/",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -180,7 +180,9 @@ def set_conf(name, conf, user=None, registry=_plugins):
 | 
			
		|||
    if not registry[name]["conf"] and not registry[name]["source"]:
 | 
			
		||||
        return
 | 
			
		||||
    conf_serializer = get_serializer_from_conf_template(
 | 
			
		||||
        registry[name]["conf"], user=user, source=registry[name]["source"],
 | 
			
		||||
        registry[name]["conf"],
 | 
			
		||||
        user=user,
 | 
			
		||||
        source=registry[name]["source"],
 | 
			
		||||
    )(data=conf)
 | 
			
		||||
    conf_serializer.is_valid(raise_exception=True)
 | 
			
		||||
    if "library" in conf_serializer.validated_data:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,7 +135,8 @@ class ChannelCreateSerializer(serializers.Serializer):
 | 
			
		|||
            metadata=validated_data["metadata"],
 | 
			
		||||
        )
 | 
			
		||||
        channel.actor = models.generate_actor(
 | 
			
		||||
            validated_data["username"], name=validated_data["name"],
 | 
			
		||||
            validated_data["username"],
 | 
			
		||||
            name=validated_data["name"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        channel.library = music_models.Library.objects.create(
 | 
			
		||||
| 
						 | 
				
			
			@ -571,7 +572,8 @@ class RssFeedSerializer(serializers.Serializer):
 | 
			
		|||
 | 
			
		||||
        # create/update the channel
 | 
			
		||||
        channel, created = models.Channel.objects.update_or_create(
 | 
			
		||||
            pk=existing.pk if existing else None, defaults=channel_defaults,
 | 
			
		||||
            pk=existing.pk if existing else None,
 | 
			
		||||
            defaults=channel_defaults,
 | 
			
		||||
        )
 | 
			
		||||
        return channel
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -773,7 +775,8 @@ class RssFeedItemSerializer(serializers.Serializer):
 | 
			
		|||
 | 
			
		||||
        # create/update the track
 | 
			
		||||
        track, created = music_models.Track.objects.update_or_create(
 | 
			
		||||
            **track_kwargs, defaults=track_defaults,
 | 
			
		||||
            **track_kwargs,
 | 
			
		||||
            defaults=track_defaults,
 | 
			
		||||
        )
 | 
			
		||||
        # optimisation for reducing SQL queries, because we cannot use select_related with
 | 
			
		||||
        # update or create, so we restore the cache by hand
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,10 @@ 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"))
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -192,7 +195,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")
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +237,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 +250,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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -157,13 +157,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"""
 | 
			
		||||
| 
						 | 
				
			
			@ -210,7 +213,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"""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -227,7 +227,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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -126,7 +126,9 @@ def get_spa_file(spa_url, name):
 | 
			
		|||
    if cached:
 | 
			
		||||
        return cached
 | 
			
		||||
 | 
			
		||||
    response = session.get_session().get(utils.join_url(spa_url, name),)
 | 
			
		||||
    response = session.get_session().get(
 | 
			
		||||
        utils.join_url(spa_url, name),
 | 
			
		||||
    )
 | 
			
		||||
    response.raise_for_status()
 | 
			
		||||
    content = response.text
 | 
			
		||||
    caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -222,7 +222,8 @@ class Attachment(models.Model):
 | 
			
		|||
        validators=[
 | 
			
		||||
            validators.ImageDimensionsValidator(min_width=50, min_height=50),
 | 
			
		||||
            validators.FileValidator(
 | 
			
		||||
                allowed_extensions=["png", "jpg", "jpeg"], max_size=1024 * 1024 * 5,
 | 
			
		||||
                allowed_extensions=["png", "jpg", "jpeg"],
 | 
			
		||||
                max_size=1024 * 1024 * 5,
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,20 +26,20 @@ def normalize_query(
 | 
			
		|||
    findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
 | 
			
		||||
    normspace=re.compile(r"\s{2,}").sub,
 | 
			
		||||
):
 | 
			
		||||
    """ Splits the query string in invidual keywords, getting rid of unecessary spaces
 | 
			
		||||
        and grouping quoted words together.
 | 
			
		||||
        Example:
 | 
			
		||||
    """Splits the query string in invidual keywords, getting rid of unecessary spaces
 | 
			
		||||
    and grouping quoted words together.
 | 
			
		||||
    Example:
 | 
			
		||||
 | 
			
		||||
        >>> normalize_query('  some random  words "with   quotes  " and   spaces')
 | 
			
		||||
        ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
 | 
			
		||||
    >>> normalize_query('  some random  words "with   quotes  " and   spaces')
 | 
			
		||||
    ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return [normspace(" ", (t[0] or t[1]).strip()) for t in findterms(query_string)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_query(query_string, search_fields):
 | 
			
		||||
    """ Returns a query, that is a combination of Q objects. That combination
 | 
			
		||||
        aims to search keywords within a model by testing the given search fields.
 | 
			
		||||
    """Returns a query, that is a combination of Q objects. That combination
 | 
			
		||||
    aims to search keywords within a model by testing the given search fields.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    query = None  # Query to search for every search term
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -310,7 +310,9 @@ class ContentSerializer(serializers.Serializer):
 | 
			
		|||
    text = serializers.CharField(
 | 
			
		||||
        max_length=models.CONTENT_TEXT_MAX_LENGTH, allow_null=True
 | 
			
		||||
    )
 | 
			
		||||
    content_type = serializers.ChoiceField(choices=models.CONTENT_TEXT_SUPPORTED_TYPES,)
 | 
			
		||||
    content_type = serializers.ChoiceField(
 | 
			
		||||
        choices=models.CONTENT_TEXT_SUPPORTED_TYPES,
 | 
			
		||||
    )
 | 
			
		||||
    html = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_html(self, o):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,7 +139,13 @@ def handshake_v2(username, password, session, api_key, api_secret, scrobble_url)
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def submit_scrobble_v2(
 | 
			
		||||
    session, track, scrobble_time, session_key, scrobble_url, api_key, api_secret,
 | 
			
		||||
    session,
 | 
			
		||||
    track,
 | 
			
		||||
    scrobble_time,
 | 
			
		||||
    session_key,
 | 
			
		||||
    scrobble_url,
 | 
			
		||||
    api_key,
 | 
			
		||||
    api_secret,
 | 
			
		||||
):
 | 
			
		||||
    params = {
 | 
			
		||||
        "method": "track.scrobble",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,8 @@ logger = logging.getLogger(__name__)
 | 
			
		|||
def get_actor_data(actor_url):
 | 
			
		||||
    logger.debug("Fetching actor %s", actor_url)
 | 
			
		||||
    response = session.get_session().get(
 | 
			
		||||
        actor_url, headers={"Accept": "application/activity+json"},
 | 
			
		||||
        actor_url,
 | 
			
		||||
        headers={"Accept": "application/activity+json"},
 | 
			
		||||
    )
 | 
			
		||||
    response.raise_for_status()
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
 | 
			
		|||
            actor = actors.get_actor(actor_url)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.info(
 | 
			
		||||
                "Discarding HTTP request from actor/domain %s, %s", actor_url, str(e),
 | 
			
		||||
                "Discarding HTTP request from actor/domain %s, %s",
 | 
			
		||||
                actor_url,
 | 
			
		||||
                str(e),
 | 
			
		||||
            )
 | 
			
		||||
            raise rest_exceptions.AuthenticationFailed(
 | 
			
		||||
                "Cannot fetch remote actor to authenticate signature"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,7 +128,9 @@ class ActorFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
 | 
			
		|||
        model = models.Actor
 | 
			
		||||
 | 
			
		||||
    class Params:
 | 
			
		||||
        with_real_keys = factory.Trait(keys=factory.LazyFunction(keys.get_key_pair),)
 | 
			
		||||
        with_real_keys = factory.Trait(
 | 
			
		||||
            keys=factory.LazyFunction(keys.get_key_pair),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @factory.post_generation
 | 
			
		||||
    def local(self, create, extracted, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -192,7 +192,9 @@ def prepare_for_serializer(payload, config, fallbacks={}):
 | 
			
		|||
            for a in aliases:
 | 
			
		||||
                try:
 | 
			
		||||
                    value = get_value(
 | 
			
		||||
                        payload[a["property"]], keep=a.get("keep"), attr=a.get("attr"),
 | 
			
		||||
                        payload[a["property"]],
 | 
			
		||||
                        keep=a.get("keep"),
 | 
			
		||||
                        attr=a.get("attr"),
 | 
			
		||||
                    )
 | 
			
		||||
                except (IndexError, KeyError):
 | 
			
		||||
                    continue
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,9 @@ def get_library_data(library_url, actor):
 | 
			
		|||
    auth = signing.get_auth(actor.private_key, actor.private_key_id)
 | 
			
		||||
    try:
 | 
			
		||||
        response = session.get_session().get(
 | 
			
		||||
            library_url, auth=auth, headers={"Accept": "application/activity+json"},
 | 
			
		||||
            library_url,
 | 
			
		||||
            auth=auth,
 | 
			
		||||
            headers={"Accept": "application/activity+json"},
 | 
			
		||||
        )
 | 
			
		||||
    except requests.ConnectionError:
 | 
			
		||||
        return {"errors": ["This library is not reachable"]}
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +32,9 @@ def get_library_data(library_url, actor):
 | 
			
		|||
def get_library_page(library, page_url, actor):
 | 
			
		||||
    auth = signing.get_auth(actor.private_key, actor.private_key_id)
 | 
			
		||||
    response = session.get_session().get(
 | 
			
		||||
        page_url, auth=auth, headers={"Accept": "application/activity+json"},
 | 
			
		||||
        page_url,
 | 
			
		||||
        auth=auth,
 | 
			
		||||
        headers={"Accept": "application/activity+json"},
 | 
			
		||||
    )
 | 
			
		||||
    serializer = serializers.CollectionPageSerializer(
 | 
			
		||||
        data=response.json(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -196,7 +196,8 @@ def inbox_create_audio(payload, context):
 | 
			
		|||
    if is_channel:
 | 
			
		||||
        channel = context["actor"].get_channel()
 | 
			
		||||
        serializer = serializers.ChannelCreateUploadSerializer(
 | 
			
		||||
            data=payload, context={"channel": channel},
 | 
			
		||||
            data=payload,
 | 
			
		||||
            context={"channel": channel},
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        serializer = serializers.UploadSerializer(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -500,7 +500,10 @@ def create_or_update_channel(actor, rss_url, attributed_to_fid, **validated_data
 | 
			
		|||
            reverse("federation:music:libraries-detail", kwargs={"uuid": uid})
 | 
			
		||||
        )
 | 
			
		||||
        library = attributed_to.libraries.create(
 | 
			
		||||
            privacy_level="everyone", name=artist_defaults["name"], fid=fid, uuid=uid,
 | 
			
		||||
            privacy_level="everyone",
 | 
			
		||||
            name=artist_defaults["name"],
 | 
			
		||||
            fid=fid,
 | 
			
		||||
            uuid=uid,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        library = artist.channel.library
 | 
			
		||||
| 
						 | 
				
			
			@ -512,7 +515,9 @@ def create_or_update_channel(actor, rss_url, attributed_to_fid, **validated_data
 | 
			
		|||
        "library": library,
 | 
			
		||||
    }
 | 
			
		||||
    channel, created = audio_models.Channel.objects.update_or_create(
 | 
			
		||||
        actor=actor, attributed_to=attributed_to, defaults=channel_defaults,
 | 
			
		||||
        actor=actor,
 | 
			
		||||
        attributed_to=attributed_to,
 | 
			
		||||
        defaults=channel_defaults,
 | 
			
		||||
    )
 | 
			
		||||
    return channel
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1734,7 +1739,8 @@ class FlagSerializer(jsonld.JsonLdSerializer):
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        report, created = moderation_models.Report.objects.update_or_create(
 | 
			
		||||
            fid=validated_data["id"], defaults=kwargs,
 | 
			
		||||
            fid=validated_data["id"],
 | 
			
		||||
            defaults=kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        moderation_signals.report_created.send(sender=None, report=report)
 | 
			
		||||
        return report
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -338,7 +338,9 @@ def fetch(fetch_obj):
 | 
			
		|||
            if not payload:
 | 
			
		||||
                return error("blocked", message="Blocked by MRF")
 | 
			
		||||
        response = session.get_session().get(
 | 
			
		||||
            auth=auth, url=url, headers={"Accept": "application/activity+json"},
 | 
			
		||||
            auth=auth,
 | 
			
		||||
            url=url,
 | 
			
		||||
            headers={"Accept": "application/activity+json"},
 | 
			
		||||
        )
 | 
			
		||||
        logger.debug("Remote answered with %s: %s", response.status_code, response.text)
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
| 
						 | 
				
			
			@ -425,7 +427,9 @@ def fetch(fetch_obj):
 | 
			
		|||
                # first page fetch is synchronous, so that at least some data is available
 | 
			
		||||
                # in the UI after subscription
 | 
			
		||||
                result = fetch_collection(
 | 
			
		||||
                    obj.actor.outbox_url, channel_id=obj.pk, max_pages=1,
 | 
			
		||||
                    obj.actor.outbox_url,
 | 
			
		||||
                    channel_id=obj.pk,
 | 
			
		||||
                    max_pages=1,
 | 
			
		||||
                )
 | 
			
		||||
            except Exception:
 | 
			
		||||
                logger.exception(
 | 
			
		||||
| 
						 | 
				
			
			@ -473,7 +477,8 @@ class PreserveSomeDataCollector(Collector):
 | 
			
		|||
@celery.app.task(name="federation.remove_actor")
 | 
			
		||||
@transaction.atomic
 | 
			
		||||
@celery.require_instance(
 | 
			
		||||
    models.Actor.objects.all(), "actor",
 | 
			
		||||
    models.Actor.objects.all(),
 | 
			
		||||
    "actor",
 | 
			
		||||
)
 | 
			
		||||
def remove_actor(actor):
 | 
			
		||||
    # Then we broadcast the info over federation. We do this *before* deleting objects
 | 
			
		||||
| 
						 | 
				
			
			@ -531,7 +536,9 @@ def match_serializer(payload, conf):
 | 
			
		|||
 | 
			
		||||
@celery.app.task(name="federation.fetch_collection")
 | 
			
		||||
@celery.require_instance(
 | 
			
		||||
    audio_models.Channel.objects.all(), "channel", allow_null=True,
 | 
			
		||||
    audio_models.Channel.objects.all(),
 | 
			
		||||
    "channel",
 | 
			
		||||
    allow_null=True,
 | 
			
		||||
)
 | 
			
		||||
def fetch_collection(url, max_pages, channel, is_page=False):
 | 
			
		||||
    actor = actors.get_service_actor()
 | 
			
		||||
| 
						 | 
				
			
			@ -564,7 +571,11 @@ def fetch_collection(url, max_pages, channel, is_page=False):
 | 
			
		|||
    for i in range(max_pages):
 | 
			
		||||
        page_url = results["next_page"]
 | 
			
		||||
        logger.debug("Handling page %s on max %s, at %s", i + 1, max_pages, page_url)
 | 
			
		||||
        page = utils.retrieve_ap_object(page_url, actor=actor, serializer_class=None,)
 | 
			
		||||
        page = utils.retrieve_ap_object(
 | 
			
		||||
            page_url,
 | 
			
		||||
            actor=actor,
 | 
			
		||||
            serializer_class=None,
 | 
			
		||||
        )
 | 
			
		||||
        try:
 | 
			
		||||
            items = page["orderedItems"]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -470,7 +470,8 @@ class IndexViewSet(FederationMixin, viewsets.GenericViewSet):
 | 
			
		|||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @action(
 | 
			
		||||
        methods=["get"], detail=False,
 | 
			
		||||
        methods=["get"],
 | 
			
		||||
        detail=False,
 | 
			
		||||
    )
 | 
			
		||||
    def libraries(self, request, *args, **kwargs):
 | 
			
		||||
        libraries = (
 | 
			
		||||
| 
						 | 
				
			
			@ -497,7 +498,8 @@ class IndexViewSet(FederationMixin, viewsets.GenericViewSet):
 | 
			
		|||
        return response.Response({}, status=200)
 | 
			
		||||
 | 
			
		||||
    @action(
 | 
			
		||||
        methods=["get"], detail=False,
 | 
			
		||||
        methods=["get"],
 | 
			
		||||
        detail=False,
 | 
			
		||||
    )
 | 
			
		||||
    def channels(self, request, *args, **kwargs):
 | 
			
		||||
        actors = (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -686,7 +686,10 @@ class ManageChannelViewSet(
 | 
			
		|||
    queryset = (
 | 
			
		||||
        audio_models.Channel.objects.all()
 | 
			
		||||
        .order_by("-id")
 | 
			
		||||
        .select_related("attributed_to", "actor",)
 | 
			
		||||
        .select_related(
 | 
			
		||||
            "attributed_to",
 | 
			
		||||
            "actor",
 | 
			
		||||
        )
 | 
			
		||||
        .prefetch_related(
 | 
			
		||||
            Prefetch(
 | 
			
		||||
                "artist",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,7 +82,8 @@ class Command(BaseCommand):
 | 
			
		|||
            content = models.Activity.objects.get(uuid=input).payload
 | 
			
		||||
        elif is_url(input):
 | 
			
		||||
            response = session.get_session().get(
 | 
			
		||||
                input, headers={"Accept": "application/activity+json"},
 | 
			
		||||
                input,
 | 
			
		||||
                headers={"Accept": "application/activity+json"},
 | 
			
		||||
            )
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
            content = response.json()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -205,7 +205,9 @@ class UserRequest(models.Model):
 | 
			
		|||
        max_length=40, choices=USER_REQUEST_STATUSES, default="pending"
 | 
			
		||||
    )
 | 
			
		||||
    submitter = models.ForeignKey(
 | 
			
		||||
        "federation.Actor", related_name="requests", on_delete=models.CASCADE,
 | 
			
		||||
        "federation.Actor",
 | 
			
		||||
        related_name="requests",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    assigned_to = models.ForeignKey(
 | 
			
		||||
        "federation.Actor",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -211,7 +211,9 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet):
 | 
			
		|||
    library = filters.UUIDFilter("library__uuid")
 | 
			
		||||
    playable = filters.BooleanFilter(field_name="_", method="filter_playable")
 | 
			
		||||
    scope = common_filters.ActorScopeFilter(
 | 
			
		||||
        actor_field="library__actor", distinct=True, library_field="library",
 | 
			
		||||
        actor_field="library__actor",
 | 
			
		||||
        distinct=True,
 | 
			
		||||
        library_field="library",
 | 
			
		||||
    )
 | 
			
		||||
    import_status = common_filters.MultipleQueryFilter(coerce=str)
 | 
			
		||||
    q = fields.SmartSearchFilter(
 | 
			
		||||
| 
						 | 
				
			
			@ -291,9 +293,13 @@ class AlbumFilter(
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class LibraryFilter(filters.FilterSet):
 | 
			
		||||
    q = fields.SearchFilter(search_fields=["name"],)
 | 
			
		||||
    q = fields.SearchFilter(
 | 
			
		||||
        search_fields=["name"],
 | 
			
		||||
    )
 | 
			
		||||
    scope = common_filters.ActorScopeFilter(
 | 
			
		||||
        actor_field="actor", distinct=True, library_field="pk",
 | 
			
		||||
        actor_field="actor",
 | 
			
		||||
        distinct=True,
 | 
			
		||||
        library_field="pk",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -364,12 +364,15 @@ class Command(BaseCommand):
 | 
			
		|||
            time_stats = ""
 | 
			
		||||
            if i > 0:
 | 
			
		||||
                time_stats = " - running for {}s, previous batch took {}s".format(
 | 
			
		||||
                    int(time.time() - start_time), int(batch_duration),
 | 
			
		||||
                    int(time.time() - start_time),
 | 
			
		||||
                    int(batch_duration),
 | 
			
		||||
                )
 | 
			
		||||
            if entries:
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    "Handling batch {} ({} items){}".format(
 | 
			
		||||
                        i + 1, len(entries), time_stats,
 | 
			
		||||
                        i + 1,
 | 
			
		||||
                        len(entries),
 | 
			
		||||
                        time_stats,
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                batch_errors = self.handle_batch(
 | 
			
		||||
| 
						 | 
				
			
			@ -536,7 +539,8 @@ class Command(BaseCommand):
 | 
			
		|||
        watchdog_queue = queue.Queue()
 | 
			
		||||
        # Set up a worker thread to process database load
 | 
			
		||||
        worker = threading.Thread(
 | 
			
		||||
            target=process_load_queue(self.stdout, **kwargs), args=(watchdog_queue,),
 | 
			
		||||
            target=process_load_queue(self.stdout, **kwargs),
 | 
			
		||||
            args=(watchdog_queue,),
 | 
			
		||||
        )
 | 
			
		||||
        worker.setDaemon(True)
 | 
			
		||||
        worker.start()
 | 
			
		||||
| 
						 | 
				
			
			@ -544,7 +548,9 @@ class Command(BaseCommand):
 | 
			
		|||
        # setup watchdog to monitor directory for trigger files
 | 
			
		||||
        patterns = ["*.{}".format(e) for e in extensions]
 | 
			
		||||
        event_handler = Watcher(
 | 
			
		||||
            stdout=self.stdout, queue=watchdog_queue, patterns=patterns,
 | 
			
		||||
            stdout=self.stdout,
 | 
			
		||||
            queue=watchdog_queue,
 | 
			
		||||
            patterns=patterns,
 | 
			
		||||
        )
 | 
			
		||||
        observer = watchdog.observers.Observer()
 | 
			
		||||
        observer.schedule(event_handler, path, recursive=recursive)
 | 
			
		||||
| 
						 | 
				
			
			@ -581,7 +587,14 @@ def prune():
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def create_upload(
 | 
			
		||||
    path, reference, library, async_, replace, in_place, dispatch_outbox, broadcast,
 | 
			
		||||
    path,
 | 
			
		||||
    reference,
 | 
			
		||||
    library,
 | 
			
		||||
    async_,
 | 
			
		||||
    replace,
 | 
			
		||||
    in_place,
 | 
			
		||||
    dispatch_outbox,
 | 
			
		||||
    broadcast,
 | 
			
		||||
):
 | 
			
		||||
    import_handler = tasks.process_upload.delay if async_ else tasks.process_upload
 | 
			
		||||
    upload = models.Upload(library=library, import_reference=reference)
 | 
			
		||||
| 
						 | 
				
			
			@ -692,7 +705,9 @@ def handle_modified(event, stdout, library, in_place, **kwargs):
 | 
			
		|||
            existing_candidates.in_place()
 | 
			
		||||
            .filter(source=source)
 | 
			
		||||
            .select_related(
 | 
			
		||||
                "track__attributed_to", "track__artist", "track__album__artist",
 | 
			
		||||
                "track__attributed_to",
 | 
			
		||||
                "track__artist",
 | 
			
		||||
                "track__album__artist",
 | 
			
		||||
            )
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -792,7 +807,12 @@ def check_updates(stdout, library, extensions, paths, batch_size):
 | 
			
		|||
    stdout.write("Found {} files to check in database!".format(total))
 | 
			
		||||
    uploads = existing.order_by("source")
 | 
			
		||||
    for i, rows in enumerate(batch(uploads.iterator(), batch_size)):
 | 
			
		||||
        stdout.write("Handling batch {} ({} items)".format(i + 1, len(rows),))
 | 
			
		||||
        stdout.write(
 | 
			
		||||
            "Handling batch {} ({} items)".format(
 | 
			
		||||
                i + 1,
 | 
			
		||||
                len(rows),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for upload in rows:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -331,7 +331,8 @@ class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
 | 
			
		|||
        )
 | 
			
		||||
        return self.annotate(
 | 
			
		||||
            duration=models.Sum(
 | 
			
		||||
                "tracks__uploads__duration", filter=Q(tracks__uploads=subquery),
 | 
			
		||||
                "tracks__uploads__duration",
 | 
			
		||||
                filter=Q(tracks__uploads=subquery),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1177,7 +1178,10 @@ class LibraryQuerySet(models.QuerySet):
 | 
			
		|||
        ).values_list("target", flat=True)
 | 
			
		||||
        followed_channels_libraries = (
 | 
			
		||||
            Follow.objects.exclude(target__channel=None)
 | 
			
		||||
            .filter(actor=actor, approved=True,)
 | 
			
		||||
            .filter(
 | 
			
		||||
                actor=actor,
 | 
			
		||||
                approved=True,
 | 
			
		||||
            )
 | 
			
		||||
            .values_list("target__channel__library", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
        return self.filter(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -173,7 +173,8 @@ def fail_import(upload, error_code, detail=None, **fields):
 | 
			
		|||
@celery.app.task(name="music.process_upload")
 | 
			
		||||
@celery.require_instance(
 | 
			
		||||
    models.Upload.objects.filter(import_status="pending").select_related(
 | 
			
		||||
        "library__actor__user", "library__channel__artist",
 | 
			
		||||
        "library__actor__user",
 | 
			
		||||
        "library__channel__artist",
 | 
			
		||||
    ),
 | 
			
		||||
    "upload",
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -239,7 +240,9 @@ def process_upload(upload, update_denormalization=True):
 | 
			
		|||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        final_metadata = collections.ChainMap(
 | 
			
		||||
            additional_data, forced_values, internal_config,
 | 
			
		||||
            additional_data,
 | 
			
		||||
            forced_values,
 | 
			
		||||
            internal_config,
 | 
			
		||||
        )
 | 
			
		||||
    try:
 | 
			
		||||
        track = get_track_from_import_metadata(
 | 
			
		||||
| 
						 | 
				
			
			@ -310,7 +313,8 @@ def process_upload(upload, update_denormalization=True):
 | 
			
		|||
    # update album cover, if needed
 | 
			
		||||
    if track.album and not track.album.attachment_cover:
 | 
			
		||||
        populate_album_cover(
 | 
			
		||||
            track.album, source=final_metadata.get("upload_source"),
 | 
			
		||||
            track.album,
 | 
			
		||||
            source=final_metadata.get("upload_source"),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if broadcast:
 | 
			
		||||
| 
						 | 
				
			
			@ -793,7 +797,9 @@ def albums_set_tags_from_tracks(ids=None, dry_run=False):
 | 
			
		|||
    if ids is not None:
 | 
			
		||||
        qs = qs.filter(pk__in=ids)
 | 
			
		||||
    data = tags_tasks.get_tags_from_foreign_key(
 | 
			
		||||
        ids=qs, foreign_key_model=models.Track, foreign_key_attr="album",
 | 
			
		||||
        ids=qs,
 | 
			
		||||
        foreign_key_model=models.Track,
 | 
			
		||||
        foreign_key_attr="album",
 | 
			
		||||
    )
 | 
			
		||||
    logger.info("Found automatic tags for %s albums…", len(data))
 | 
			
		||||
    if dry_run:
 | 
			
		||||
| 
						 | 
				
			
			@ -801,7 +807,8 @@ def albums_set_tags_from_tracks(ids=None, dry_run=False):
 | 
			
		|||
        return
 | 
			
		||||
 | 
			
		||||
    tags_tasks.add_tags_batch(
 | 
			
		||||
        data, model=models.Album,
 | 
			
		||||
        data,
 | 
			
		||||
        model=models.Album,
 | 
			
		||||
    )
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -815,7 +822,9 @@ def artists_set_tags_from_tracks(ids=None, dry_run=False):
 | 
			
		|||
    if ids is not None:
 | 
			
		||||
        qs = qs.filter(pk__in=ids)
 | 
			
		||||
    data = tags_tasks.get_tags_from_foreign_key(
 | 
			
		||||
        ids=qs, foreign_key_model=models.Track, foreign_key_attr="artist",
 | 
			
		||||
        ids=qs,
 | 
			
		||||
        foreign_key_model=models.Track,
 | 
			
		||||
        foreign_key_attr="artist",
 | 
			
		||||
    )
 | 
			
		||||
    logger.info("Found automatic tags for %s artists…", len(data))
 | 
			
		||||
    if dry_run:
 | 
			
		||||
| 
						 | 
				
			
			@ -823,7 +832,8 @@ def artists_set_tags_from_tracks(ids=None, dry_run=False):
 | 
			
		|||
        return
 | 
			
		||||
 | 
			
		||||
    tags_tasks.add_tags_batch(
 | 
			
		||||
        data, model=models.Artist,
 | 
			
		||||
        data,
 | 
			
		||||
        model=models.Artist,
 | 
			
		||||
    )
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,8 @@ def get_libraries(filter_uploads):
 | 
			
		|||
        uploads = filter_uploads(obj, uploads)
 | 
			
		||||
        uploads = uploads.playable_by(actor)
 | 
			
		||||
        qs = models.Library.objects.filter(
 | 
			
		||||
            pk__in=uploads.values_list("library", flat=True), channel=None,
 | 
			
		||||
            pk__in=uploads.values_list("library", flat=True),
 | 
			
		||||
            channel=None,
 | 
			
		||||
        ).annotate(_uploads_count=Count("uploads"))
 | 
			
		||||
        qs = qs.prefetch_related("actor")
 | 
			
		||||
        page = self.paginate_queryset(qs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -316,7 +316,9 @@ class LibraryRadio(RelatedObjectRadio):
 | 
			
		|||
 | 
			
		||||
    def get_queryset(self, **kwargs):
 | 
			
		||||
        qs = super().get_queryset(**kwargs)
 | 
			
		||||
        actor_uploads = Upload.objects.filter(library=self.session.related_object,)
 | 
			
		||||
        actor_uploads = Upload.objects.filter(
 | 
			
		||||
            library=self.session.related_object,
 | 
			
		||||
        )
 | 
			
		||||
        return qs.filter(pk__in=actor_uploads.values("track"))
 | 
			
		||||
 | 
			
		||||
    def get_related_object_id_repr(self, obj):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,9 @@ def generate_scoped_token(user_id, user_secret, scopes):
 | 
			
		|||
def authenticate_scoped_token(token):
 | 
			
		||||
    try:
 | 
			
		||||
        payload = signing.loads(
 | 
			
		||||
            token, salt="scoped_tokens", max_age=settings.SCOPED_TOKENS_MAX_AGE,
 | 
			
		||||
            token,
 | 
			
		||||
            salt="scoped_tokens",
 | 
			
		||||
            max_age=settings.SCOPED_TOKENS_MAX_AGE,
 | 
			
		||||
        )
 | 
			
		||||
    except signing.BadSignature:
 | 
			
		||||
        raise exceptions.AuthenticationFailed("Invalid token signature")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,7 +52,8 @@ def test_channel_create(logged_in_api_client):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "field", ["uuid", "actor.preferred_username", "actor.full_username"],
 | 
			
		||||
    "field",
 | 
			
		||||
    ["uuid", "actor.preferred_username", "actor.full_username"],
 | 
			
		||||
)
 | 
			
		||||
def test_channel_detail(field, factories, logged_in_api_client):
 | 
			
		||||
    channel = factories["audio.Channel"](
 | 
			
		||||
| 
						 | 
				
			
			@ -423,7 +424,10 @@ def test_subscribe_to_rss_creates_channel(factories, logged_in_api_client, mocke
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def test_refresh_channel_when_param_is_true(
 | 
			
		||||
    factories, mocker, logged_in_api_client, queryset_equal_queries,
 | 
			
		||||
    factories,
 | 
			
		||||
    mocker,
 | 
			
		||||
    logged_in_api_client,
 | 
			
		||||
    queryset_equal_queries,
 | 
			
		||||
):
 | 
			
		||||
    obj = factories["audio.Channel"]()
 | 
			
		||||
    refetch_obj = mocker.patch(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,7 +57,12 @@ from funkwhale_api.cli import users
 | 
			
		|||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            ("users", "rm"),
 | 
			
		||||
            ("testuser1", "testuser2", "--no-input", "--hard",),
 | 
			
		||||
            (
 | 
			
		||||
                "testuser1",
 | 
			
		||||
                "testuser2",
 | 
			
		||||
                "--no-input",
 | 
			
		||||
                "--hard",
 | 
			
		||||
            ),
 | 
			
		||||
            [
 | 
			
		||||
                (
 | 
			
		||||
                    users,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -301,13 +301,17 @@ def test_rewrite_manifest_json_url(link, new_url, expected, mocker, settings):
 | 
			
		|||
    request = mocker.Mock(path="/", META={})
 | 
			
		||||
    mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
 | 
			
		||||
    mocker.patch.object(
 | 
			
		||||
        middleware, "get_default_head_tags", return_value=[],
 | 
			
		||||
        middleware,
 | 
			
		||||
        "get_default_head_tags",
 | 
			
		||||
        return_value=[],
 | 
			
		||||
    )
 | 
			
		||||
    response = middleware.serve_spa(request)
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    expected_html = "<html><head><link rel=before>{}<link rel=after>\n\n</head></html>".format(
 | 
			
		||||
        expected
 | 
			
		||||
    expected_html = (
 | 
			
		||||
        "<html><head><link rel=before>{}<link rel=after>\n\n</head></html>".format(
 | 
			
		||||
            expected
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    assert response.content == expected_html.encode()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -319,7 +323,9 @@ def test_rewrite_manifest_json_url_rewrite_disabled(mocker, settings):
 | 
			
		|||
    request = mocker.Mock(path="/", META={})
 | 
			
		||||
    mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
 | 
			
		||||
    mocker.patch.object(
 | 
			
		||||
        middleware, "get_default_head_tags", return_value=[],
 | 
			
		||||
        middleware,
 | 
			
		||||
        "get_default_head_tags",
 | 
			
		||||
        return_value=[],
 | 
			
		||||
    )
 | 
			
		||||
    response = middleware.serve_spa(request)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -338,13 +344,17 @@ def test_rewrite_manifest_json_url_rewrite_default_url(mocker, settings):
 | 
			
		|||
    request = mocker.Mock(path="/", META={})
 | 
			
		||||
    mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
 | 
			
		||||
    mocker.patch.object(
 | 
			
		||||
        middleware, "get_default_head_tags", return_value=[],
 | 
			
		||||
        middleware,
 | 
			
		||||
        "get_default_head_tags",
 | 
			
		||||
        return_value=[],
 | 
			
		||||
    )
 | 
			
		||||
    response = middleware.serve_spa(request)
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    expected_html = '<html><head><link rel=manifest href="{}">\n\n</head></html>'.format(
 | 
			
		||||
        expected_url
 | 
			
		||||
    expected_html = (
 | 
			
		||||
        '<html><head><link rel=manifest href="{}">\n\n</head></html>'.format(
 | 
			
		||||
            expected_url
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    assert response.content == expected_html.encode()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -410,10 +420,34 @@ def test_get_request_head_tags_calls_view_with_proper_arg_when_accept_header_set
 | 
			
		|||
            "username",
 | 
			
		||||
            "actor.preferred_username",
 | 
			
		||||
        ),
 | 
			
		||||
        ("music.Artist", {}, "library_artist", "pk", "pk",),
 | 
			
		||||
        ("music.Album", {}, "library_album", "pk", "pk",),
 | 
			
		||||
        ("music.Track", {}, "library_track", "pk", "pk",),
 | 
			
		||||
        ("music.Library", {}, "library_library", "uuid", "uuid",),
 | 
			
		||||
        (
 | 
			
		||||
            "music.Artist",
 | 
			
		||||
            {},
 | 
			
		||||
            "library_artist",
 | 
			
		||||
            "pk",
 | 
			
		||||
            "pk",
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "music.Album",
 | 
			
		||||
            {},
 | 
			
		||||
            "library_album",
 | 
			
		||||
            "pk",
 | 
			
		||||
            "pk",
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "music.Track",
 | 
			
		||||
            {},
 | 
			
		||||
            "library_track",
 | 
			
		||||
            "pk",
 | 
			
		||||
            "pk",
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            "music.Library",
 | 
			
		||||
            {},
 | 
			
		||||
            "library_library",
 | 
			
		||||
            "uuid",
 | 
			
		||||
            "uuid",
 | 
			
		||||
        ),
 | 
			
		||||
        # when a track as a public upload, we should redirect to the upload instead
 | 
			
		||||
        ("music.Upload", {"playable": True}, "library_track", "pk", "track.pk"),
 | 
			
		||||
    ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -257,7 +257,9 @@ def test_update_library_follow_approved_create_entries(
 | 
			
		|||
        assert list(music_models.Track.objects.playable_by(actor)) == expected_tracks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_update_library_follow_delete_delete_denormalization_entries(factories,):
 | 
			
		||||
def test_update_library_follow_delete_delete_denormalization_entries(
 | 
			
		||||
    factories,
 | 
			
		||||
):
 | 
			
		||||
    updated_playable_tracks = {"owner": [0], "follower": []}
 | 
			
		||||
    actors = {
 | 
			
		||||
        "owner": factories["federation.Actor"](local=True),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -354,7 +354,10 @@ def test_inbox_create_audio_channel(factories, mocker):
 | 
			
		|||
    activity = factories["federation.Activity"]()
 | 
			
		||||
    channel = factories["audio.Channel"]()
 | 
			
		||||
    album = factories["music.Album"](artist=channel.artist)
 | 
			
		||||
    upload = factories["music.Upload"](track__album=album, library=channel.library,)
 | 
			
		||||
    upload = factories["music.Upload"](
 | 
			
		||||
        track__album=album,
 | 
			
		||||
        library=channel.library,
 | 
			
		||||
    )
 | 
			
		||||
    payload = {
 | 
			
		||||
        "@context": jsonld.get_default_context(),
 | 
			
		||||
        "type": "Create",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -805,7 +805,9 @@ def test_activity_pub_album_serializer_to_ap(factories):
 | 
			
		|||
 | 
			
		||||
def test_activity_pub_album_serializer_to_ap_channel_artist(factories):
 | 
			
		||||
    channel = factories["audio.Channel"]()
 | 
			
		||||
    album = factories["music.Album"](artist=channel.artist,)
 | 
			
		||||
    album = factories["music.Album"](
 | 
			
		||||
        artist=channel.artist,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    serializer = serializers.AlbumSerializer(album)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -530,7 +530,9 @@ def test_fetch_channel_actor_returns_channel_and_fetch_outbox(
 | 
			
		|||
    assert fetch.status == "finished"
 | 
			
		||||
    assert fetch.object == obj
 | 
			
		||||
    fetch_collection.assert_called_once_with(
 | 
			
		||||
        obj.actor.outbox_url, channel_id=obj.pk, max_pages=1,
 | 
			
		||||
        obj.actor.outbox_url,
 | 
			
		||||
        channel_id=obj.pk,
 | 
			
		||||
        max_pages=1,
 | 
			
		||||
    )
 | 
			
		||||
    fetch_collection_delayed.assert_called_once_with(
 | 
			
		||||
        "http://outbox.url/page2",
 | 
			
		||||
| 
						 | 
				
			
			@ -655,7 +657,10 @@ def test_fetch_collection(mocker, r_mock):
 | 
			
		|||
    r_mock.get(payloads["outbox"]["id"], json=payloads["outbox"])
 | 
			
		||||
    r_mock.get(payloads["outbox"]["first"], json=payloads["page1"])
 | 
			
		||||
    r_mock.get(payloads["page1"]["next"], json=payloads["page2"])
 | 
			
		||||
    result = tasks.fetch_collection(payloads["outbox"]["id"], max_pages=2,)
 | 
			
		||||
    result = tasks.fetch_collection(
 | 
			
		||||
        payloads["outbox"]["id"],
 | 
			
		||||
        max_pages=2,
 | 
			
		||||
    )
 | 
			
		||||
    assert result["items"] == [
 | 
			
		||||
        payloads["page1"]["orderedItems"][2],
 | 
			
		||||
        payloads["page2"]["orderedItems"][1],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -296,7 +296,8 @@ def test_reel2bits_upload_delete(factories):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    routes.inbox_delete(
 | 
			
		||||
        payload, context={"actor": actor, "raise_exception": True, "activity": payload},
 | 
			
		||||
        payload,
 | 
			
		||||
        context={"actor": actor, "raise_exception": True, "activity": payload},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(upload.track.DoesNotExist):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,10 @@ def test_authenticate_skips_anonymous_fetch_when_allow_list_enabled(
 | 
			
		|||
):
 | 
			
		||||
    preferences["moderation__allow_list_enabled"] = True
 | 
			
		||||
    library = factories["music.Library"]()
 | 
			
		||||
    url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid},)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:music:libraries-detail",
 | 
			
		||||
        kwargs={"uuid": library.uuid},
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url)
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 403
 | 
			
		||||
| 
						 | 
				
			
			@ -470,7 +473,10 @@ def test_upload_retrieve_redirects_to_html_if_header_set(
 | 
			
		|||
):
 | 
			
		||||
    upload = factories["music.Upload"](library__local=True, playable=True)
 | 
			
		||||
 | 
			
		||||
    url = reverse("federation:music:uploads-detail", kwargs={"uuid": upload.uuid},)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:music:uploads-detail",
 | 
			
		||||
        kwargs={"uuid": upload.uuid},
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url, HTTP_ACCEPT="text/html")
 | 
			
		||||
    expected_url = utils.join_url(
 | 
			
		||||
        settings.FUNKWHALE_URL,
 | 
			
		||||
| 
						 | 
				
			
			@ -485,7 +491,10 @@ def test_track_retrieve_redirects_to_html_if_header_set(
 | 
			
		|||
):
 | 
			
		||||
    track = factories["music.Track"](local=True)
 | 
			
		||||
 | 
			
		||||
    url = reverse("federation:music:tracks-detail", kwargs={"uuid": track.uuid},)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:music:tracks-detail",
 | 
			
		||||
        kwargs={"uuid": track.uuid},
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url, HTTP_ACCEPT="text/html")
 | 
			
		||||
    expected_url = utils.join_url(
 | 
			
		||||
        settings.FUNKWHALE_URL,
 | 
			
		||||
| 
						 | 
				
			
			@ -500,7 +509,10 @@ def test_album_retrieve_redirects_to_html_if_header_set(
 | 
			
		|||
):
 | 
			
		||||
    album = factories["music.Album"](local=True)
 | 
			
		||||
 | 
			
		||||
    url = reverse("federation:music:albums-detail", kwargs={"uuid": album.uuid},)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:music:albums-detail",
 | 
			
		||||
        kwargs={"uuid": album.uuid},
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url, HTTP_ACCEPT="text/html")
 | 
			
		||||
    expected_url = utils.join_url(
 | 
			
		||||
        settings.FUNKWHALE_URL,
 | 
			
		||||
| 
						 | 
				
			
			@ -515,7 +527,10 @@ def test_artist_retrieve_redirects_to_html_if_header_set(
 | 
			
		|||
):
 | 
			
		||||
    artist = factories["music.Artist"](local=True)
 | 
			
		||||
 | 
			
		||||
    url = reverse("federation:music:artists-detail", kwargs={"uuid": artist.uuid},)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:music:artists-detail",
 | 
			
		||||
        kwargs={"uuid": artist.uuid},
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url, HTTP_ACCEPT="text/html")
 | 
			
		||||
    expected_url = utils.join_url(
 | 
			
		||||
        settings.FUNKWHALE_URL,
 | 
			
		||||
| 
						 | 
				
			
			@ -548,7 +563,9 @@ def test_index_channels_retrieve(factories, api_client):
 | 
			
		|||
        },
 | 
			
		||||
    ).data
 | 
			
		||||
 | 
			
		||||
    url = reverse("federation:index:index-channels",)
 | 
			
		||||
    url = reverse(
 | 
			
		||||
        "federation:index:index-channels",
 | 
			
		||||
    )
 | 
			
		||||
    response = api_client.get(url)
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,7 +66,8 @@ def test_signup_request_pending_sends_email_to_mods(factories, mailoutbox, setti
 | 
			
		|||
    for i, mod in enumerate([mod1, mod2]):
 | 
			
		||||
        m = mailoutbox[i]
 | 
			
		||||
        assert m.subject == "[{} moderation] New sign-up request from {}".format(
 | 
			
		||||
            settings.FUNKWHALE_HOSTNAME, signup_request.submitter.preferred_username,
 | 
			
		||||
            settings.FUNKWHALE_HOSTNAME,
 | 
			
		||||
            signup_request.submitter.preferred_username,
 | 
			
		||||
        )
 | 
			
		||||
        assert detail_url in m.body
 | 
			
		||||
        assert unresolved_requests_url in m.body
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +92,8 @@ def test_approved_request_sends_email_to_submitter_and_set_active(
 | 
			
		|||
    m = mailoutbox[-1]
 | 
			
		||||
    login_url = federation_utils.full_url("/login")
 | 
			
		||||
    assert m.subject == "Welcome to {}, {}!".format(
 | 
			
		||||
        settings.FUNKWHALE_HOSTNAME, signup_request.submitter.preferred_username,
 | 
			
		||||
        settings.FUNKWHALE_HOSTNAME,
 | 
			
		||||
        signup_request.submitter.preferred_username,
 | 
			
		||||
    )
 | 
			
		||||
    assert login_url in m.body
 | 
			
		||||
    assert list(m.to) == [user.email]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -213,7 +213,8 @@ def test_mutation_set_attachment_cover(factory_name, factories, now, mocker):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "factory_name", ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
    "factory_name",
 | 
			
		||||
    ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
)
 | 
			
		||||
def test_album_mutation_description(factory_name, factories, mocker):
 | 
			
		||||
    mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +237,8 @@ def test_album_mutation_description(factory_name, factories, mocker):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "factory_name", ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
    "factory_name",
 | 
			
		||||
    ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
)
 | 
			
		||||
def test_mutation_description_keep_tags(factory_name, factories, mocker):
 | 
			
		||||
    mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
 | 
			
		||||
| 
						 | 
				
			
			@ -256,14 +258,17 @@ def test_mutation_description_keep_tags(factory_name, factories, mocker):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "factory_name", ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
    "factory_name",
 | 
			
		||||
    ["music.Track", "music.Album", "music.Artist"],
 | 
			
		||||
)
 | 
			
		||||
def test_mutation_tags_keep_descriptions(factory_name, factories, mocker):
 | 
			
		||||
    mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
 | 
			
		||||
    content = factories["common.Content"]()
 | 
			
		||||
    obj = factories[factory_name](description=content)
 | 
			
		||||
    mutation = factories["common.Mutation"](
 | 
			
		||||
        type="update", target=obj, payload={"tags": ["punk", "rock"]},
 | 
			
		||||
        type="update",
 | 
			
		||||
        target=obj,
 | 
			
		||||
        payload={"tags": ["punk", "rock"]},
 | 
			
		||||
    )
 | 
			
		||||
    mutation.apply()
 | 
			
		||||
    obj.refresh_from_db()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -427,7 +427,8 @@ def test_upload_with_channel(factories, uploaded_audio_file):
 | 
			
		|||
        "import_status": "draft",
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    assert serializer.is_valid(raise_exception=True) is True
 | 
			
		||||
    upload = serializer.save()
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +444,8 @@ def test_upload_with_not_owned_channel_fails(factories, uploaded_audio_file):
 | 
			
		|||
        "audio_file": uploaded_audio_file,
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    assert serializer.is_valid() is False
 | 
			
		||||
    assert "channel" in serializer.errors
 | 
			
		||||
| 
						 | 
				
			
			@ -457,7 +459,8 @@ def test_upload_with_not_owned_library_fails(factories, uploaded_audio_file):
 | 
			
		|||
        "audio_file": uploaded_audio_file,
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    assert serializer.is_valid() is False
 | 
			
		||||
    assert "library" in serializer.errors
 | 
			
		||||
| 
						 | 
				
			
			@ -469,7 +472,8 @@ def test_upload_requires_library_or_channel(factories, uploaded_audio_file):
 | 
			
		|||
        "audio_file": uploaded_audio_file,
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(
 | 
			
		||||
| 
						 | 
				
			
			@ -491,7 +495,8 @@ def test_upload_requires_library_or_channel_but_not_both(
 | 
			
		|||
        "channel": channel.uuid,
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    with pytest.raises(
 | 
			
		||||
        serializers.serializers.ValidationError,
 | 
			
		||||
| 
						 | 
				
			
			@ -547,7 +552,8 @@ def test_upload_with_channel_keeps_import_metadata(factories, uploaded_audio_fil
 | 
			
		|||
        "import_metadata": {"title": "hello"},
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    assert serializer.is_valid(raise_exception=True) is True
 | 
			
		||||
    upload = serializer.save()
 | 
			
		||||
| 
						 | 
				
			
			@ -564,7 +570,8 @@ def test_upload_with_channel_validates_import_metadata(factories, uploaded_audio
 | 
			
		|||
        "import_metadata": {"title": None},
 | 
			
		||||
    }
 | 
			
		||||
    serializer = serializers.UploadForOwnerSerializer(
 | 
			
		||||
        data=data, context={"user": user},
 | 
			
		||||
        data=data,
 | 
			
		||||
        context={"user": user},
 | 
			
		||||
    )
 | 
			
		||||
    with pytest.raises(serializers.serializers.ValidationError):
 | 
			
		||||
        assert serializer.is_valid(raise_exception=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1082,7 +1082,9 @@ def test_process_channel_upload_forces_artist_and_attributed_to(
 | 
			
		|||
    upload.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
    expected_final_metadata = tasks.collections.ChainMap(
 | 
			
		||||
        {"upload_source": None}, expected_forced_values, {"funkwhale": {}},
 | 
			
		||||
        {"upload_source": None},
 | 
			
		||||
        expected_forced_values,
 | 
			
		||||
        {"funkwhale": {}},
 | 
			
		||||
    )
 | 
			
		||||
    assert upload.import_status == "finished"
 | 
			
		||||
    get_track_from_import_metadata.assert_called_once_with(
 | 
			
		||||
| 
						 | 
				
			
			@ -1175,7 +1177,8 @@ def test_tag_albums_from_tracks(queryset_equal_queries, factories, mocker):
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
    add_tags_batch.assert_called_once_with(
 | 
			
		||||
        get_tags_from_foreign_key.return_value, model=models.Album,
 | 
			
		||||
        get_tags_from_foreign_key.return_value,
 | 
			
		||||
        model=models.Album,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1200,7 +1203,8 @@ def test_tag_artists_from_tracks(queryset_equal_queries, factories, mocker):
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
    add_tags_batch.assert_called_once_with(
 | 
			
		||||
        get_tags_from_foreign_key.return_value, model=models.Artist,
 | 
			
		||||
        get_tags_from_foreign_key.return_value,
 | 
			
		||||
        model=models.Artist,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,8 @@ def test_playlist_inherits_user_privacy(logged_in_api_client):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "name,method", [("api:v1:playlists-list", "post")],
 | 
			
		||||
    "name,method",
 | 
			
		||||
    [("api:v1:playlists-list", "post")],
 | 
			
		||||
)
 | 
			
		||||
def test_url_requires_login(name, method, factories, api_client):
 | 
			
		||||
    url = reverse(name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -137,8 +137,12 @@ def test_get_confs_user(factories):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def test_filter_is_called_with_plugin_conf(mocker, factories):
 | 
			
		||||
    plugins.get_plugin_config("test1",)
 | 
			
		||||
    plugins.get_plugin_config("test2",)
 | 
			
		||||
    plugins.get_plugin_config(
 | 
			
		||||
        "test1",
 | 
			
		||||
    )
 | 
			
		||||
    plugins.get_plugin_config(
 | 
			
		||||
        "test2",
 | 
			
		||||
    )
 | 
			
		||||
    factories["common.PluginConfiguration"](code="test1", enabled=True)
 | 
			
		||||
    factories["common.PluginConfiguration"](
 | 
			
		||||
        code="test2", conf={"foo": "baz"}, enabled=True
 | 
			
		||||
| 
						 | 
				
			
			@ -411,7 +415,10 @@ def test_set_plugin_source_conf_valid(factories):
 | 
			
		|||
def test_can_trigger_scan(logged_in_api_client, mocker, factories):
 | 
			
		||||
    library = factories["music.Library"](actor=logged_in_api_client.user.create_actor())
 | 
			
		||||
    plugin = plugins.get_plugin_config(
 | 
			
		||||
        name="test_plugin", description="Hello world", conf=[], source=True,
 | 
			
		||||
        name="test_plugin",
 | 
			
		||||
        description="Hello world",
 | 
			
		||||
        conf=[],
 | 
			
		||||
        source=True,
 | 
			
		||||
    )
 | 
			
		||||
    handler = mocker.Mock()
 | 
			
		||||
    plugins.register_hook(plugins.SCAN, plugin)(handler)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,8 @@ def test_add_tags_batch(factories):
 | 
			
		|||
    data = {artist.pk: [rock_tag.pk, rap_tag.pk]}
 | 
			
		||||
 | 
			
		||||
    tasks.add_tags_batch(
 | 
			
		||||
        data, model=artist.__class__,
 | 
			
		||||
        data,
 | 
			
		||||
        model=artist.__class__,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert artist.get_tags() == ["Rap", "Rock"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -297,7 +297,10 @@ def test_handle_modified_skips_existing_checksum(tmpfile, factories, mocker):
 | 
			
		|||
        import_status="finished",
 | 
			
		||||
    )
 | 
			
		||||
    import_files.handle_modified(
 | 
			
		||||
        event=event, stdout=stdout, library=library, in_place=True,
 | 
			
		||||
        event=event,
 | 
			
		||||
        stdout=stdout,
 | 
			
		||||
        library=library,
 | 
			
		||||
        in_place=True,
 | 
			
		||||
    )
 | 
			
		||||
    assert library.uploads.count() == 1
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -322,10 +325,14 @@ def test_handle_modified_update_existing_path_if_found(tmpfile, factories, mocke
 | 
			
		|||
        audio_file=None,
 | 
			
		||||
    )
 | 
			
		||||
    import_files.handle_modified(
 | 
			
		||||
        event=event, stdout=stdout, library=library, in_place=True,
 | 
			
		||||
        event=event,
 | 
			
		||||
        stdout=stdout,
 | 
			
		||||
        library=library,
 | 
			
		||||
        in_place=True,
 | 
			
		||||
    )
 | 
			
		||||
    update_track_metadata.assert_called_once_with(
 | 
			
		||||
        get_metadata.return_value, upload.track,
 | 
			
		||||
        get_metadata.return_value,
 | 
			
		||||
        upload.track,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -349,7 +356,10 @@ def test_handle_modified_update_existing_path_if_found_and_attributed_to(
 | 
			
		|||
        audio_file=None,
 | 
			
		||||
    )
 | 
			
		||||
    import_files.handle_modified(
 | 
			
		||||
        event=event, stdout=stdout, library=library, in_place=True,
 | 
			
		||||
        event=event,
 | 
			
		||||
        stdout=stdout,
 | 
			
		||||
        library=library,
 | 
			
		||||
        in_place=True,
 | 
			
		||||
    )
 | 
			
		||||
    update_track_metadata.assert_not_called()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,9 @@ def test_generate_scoped_token(mocker):
 | 
			
		|||
    dumps = mocker.patch("django.core.signing.dumps")
 | 
			
		||||
 | 
			
		||||
    result = authentication.generate_scoped_token(
 | 
			
		||||
        user_id=42, user_secret="hello", scopes=["read"],
 | 
			
		||||
        user_id=42,
 | 
			
		||||
        user_secret="hello",
 | 
			
		||||
        scopes=["read"],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert result == dumps.return_value
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								docs/conf.py
								
								
								
								
							
							
						
						
									
										18
									
								
								docs/conf.py
								
								
								
								
							| 
						 | 
				
			
			@ -47,7 +47,13 @@ for key, value in FUNKWHALE_CONFIG.items():
 | 
			
		|||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 | 
			
		||||
# ones.
 | 
			
		||||
extensions = ["sphinx.ext.graphviz", "sphinx.ext.autodoc"]
 | 
			
		||||
autodoc_mock_imports = ["celery", "django_auth_ldap", "ldap", "persisting_theory", "rest_framework"]
 | 
			
		||||
autodoc_mock_imports = [
 | 
			
		||||
    "celery",
 | 
			
		||||
    "django_auth_ldap",
 | 
			
		||||
    "ldap",
 | 
			
		||||
    "persisting_theory",
 | 
			
		||||
    "rest_framework",
 | 
			
		||||
]
 | 
			
		||||
add_module_names = False
 | 
			
		||||
# Add any paths that contain templates here, relative to this directory.
 | 
			
		||||
templates_path = ["_templates"]
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +113,7 @@ html_theme = "sphinx_rtd_theme"
 | 
			
		|||
# further.  For a list of options available for each theme, see the
 | 
			
		||||
# documentation.
 | 
			
		||||
#
 | 
			
		||||
#html_theme_options = {}
 | 
			
		||||
# html_theme_options = {}
 | 
			
		||||
html_context = {
 | 
			
		||||
    "display_gitlab": True,
 | 
			
		||||
    "gitlab_host": "dev.funkwhale.audio",
 | 
			
		||||
| 
						 | 
				
			
			@ -152,7 +158,13 @@ latex_elements = {
 | 
			
		|||
# (source start file, target name, title,
 | 
			
		||||
#  author, documentclass [howto, manual, or own class]).
 | 
			
		||||
latex_documents = [
 | 
			
		||||
    (root_doc, "funkwhale.tex", "funkwhale Documentation", "The Funkwhale Collective", "manual")
 | 
			
		||||
    (
 | 
			
		||||
        root_doc,
 | 
			
		||||
        "funkwhale.tex",
 | 
			
		||||
        "funkwhale Documentation",
 | 
			
		||||
        "The Funkwhale Collective",
 | 
			
		||||
        "manual",
 | 
			
		||||
    )
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,7 +174,7 @@ def discard_unused_icons(rule):
 | 
			
		|||
        ".gitlab",
 | 
			
		||||
        ".chevron",
 | 
			
		||||
        ".right",
 | 
			
		||||
        ".left"
 | 
			
		||||
        ".left",
 | 
			
		||||
    ]
 | 
			
		||||
    if ":before" not in rule["lines"][0]:
 | 
			
		||||
        return False
 | 
			
		||||
| 
						 | 
				
			
			@ -355,7 +355,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ".ui.checkbox input:focus ~ label",
 | 
			
		||||
            ".ui.toggle.checkbox input:focus:checked ~ label",
 | 
			
		||||
            ".ui.checkbox input:active ~ label",
 | 
			
		||||
        ): [("color", "var(--form-label-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--form-label-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.toggle.checkbox label:before",): [
 | 
			
		||||
            ("background", "var(--input-background)"),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			@ -365,7 +367,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ("border-top", "var(--divider)"),
 | 
			
		||||
            ("border-bottom", "var(--divider)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.divider",): [("color", "var(--text-color)"),],
 | 
			
		||||
        (".ui.divider",): [
 | 
			
		||||
            ("color", "var(--text-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "dimmer": {
 | 
			
		||||
        (".ui.inverted.dimmer",): [
 | 
			
		||||
| 
						 | 
				
			
			@ -374,7 +378,12 @@ REPLACEMENTS = {
 | 
			
		|||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "dropdown": {
 | 
			
		||||
        "skip": [".error", ".info", ".success", ".warning",],
 | 
			
		||||
        "skip": [
 | 
			
		||||
            ".error",
 | 
			
		||||
            ".info",
 | 
			
		||||
            ".success",
 | 
			
		||||
            ".warning",
 | 
			
		||||
        ],
 | 
			
		||||
        (
 | 
			
		||||
            ".ui.selection.dropdown",
 | 
			
		||||
            ".ui.selection.visible.dropdown > .text:not(.default)",
 | 
			
		||||
| 
						 | 
				
			
			@ -383,7 +392,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ("background", "var(--dropdown-background)"),
 | 
			
		||||
            ("color", "var(--dropdown-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.dropdown .menu > .item",): [("color", "var(--dropdown-item-color)"),],
 | 
			
		||||
        (".ui.dropdown .menu > .item",): [
 | 
			
		||||
            ("color", "var(--dropdown-item-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.dropdown .menu > .item:hover",): [
 | 
			
		||||
            ("color", "var(--dropdown-item-hover-color)"),
 | 
			
		||||
            ("background", "var(--dropdown-item-hover-background)"),
 | 
			
		||||
| 
						 | 
				
			
			@ -395,10 +406,18 @@ REPLACEMENTS = {
 | 
			
		|||
        (".ui.dropdown .menu > .header:not(.ui)",): [
 | 
			
		||||
            ("color", "var(--dropdown-header-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.dropdown .menu > .divider",): [("border-top", "var(--divider)"),],
 | 
			
		||||
        (".ui.dropdown .menu > .divider",): [
 | 
			
		||||
            ("border-top", "var(--divider)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
        "skip": [".inverted", ".success", ".warning", ".error", ".info",],
 | 
			
		||||
        "skip": [
 | 
			
		||||
            ".inverted",
 | 
			
		||||
            ".success",
 | 
			
		||||
            ".warning",
 | 
			
		||||
            ".error",
 | 
			
		||||
            ".info",
 | 
			
		||||
        ],
 | 
			
		||||
        ('.ui.form input[type="text"]', ".ui.form select", ".ui.input textarea"): [
 | 
			
		||||
            ("background", "var(--input-background)"),
 | 
			
		||||
            ("color", "var(--input-color)"),
 | 
			
		||||
| 
						 | 
				
			
			@ -415,12 +434,16 @@ REPLACEMENTS = {
 | 
			
		|||
            ".ui.form ::-webkit-input-placeholder",
 | 
			
		||||
            ".ui.form :-ms-input-placeholder",
 | 
			
		||||
            ".ui.form ::-moz-placeholder",
 | 
			
		||||
        ): [("color", "var(--input-placeholder-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--input-placeholder-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (
 | 
			
		||||
            ".ui.form :focus::-webkit-input-placeholder",
 | 
			
		||||
            ".ui.form :focus:-ms-input-placeholder",
 | 
			
		||||
            ".ui.form :focus::-moz-placeholder",
 | 
			
		||||
        ): [("color", "var(--input-focus-placeholder-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--input-focus-placeholder-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.form .field > label", ".ui.form .inline.fields .field > label",): [
 | 
			
		||||
            ("color", "var(--form-label-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			@ -475,16 +498,24 @@ REPLACEMENTS = {
 | 
			
		|||
            ".ui.input > input::-webkit-input-placeholder",
 | 
			
		||||
            ".ui.input > input::-moz-placeholder",
 | 
			
		||||
            ".ui.input > input:-ms-input-placeholder",
 | 
			
		||||
        ): [("color", "var(--input-placeholder-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--input-placeholder-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (
 | 
			
		||||
            ".ui.input > input:focus::-webkit-input-placeholder",
 | 
			
		||||
            ".ui.input > input:focus::-moz-placeholder",
 | 
			
		||||
            ".ui.input > input:focus:-ms-input-placeholder",
 | 
			
		||||
        ): [("color", "var(--input-focus-placeholder-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--input-focus-placeholder-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "item": {
 | 
			
		||||
        (".ui.divided.items > .item",): [("border-top", "var(--divider)"),],
 | 
			
		||||
        (".ui.items > .item > .content",): [("color", "var(--text-color)"),],
 | 
			
		||||
        (".ui.divided.items > .item",): [
 | 
			
		||||
            ("border-top", "var(--divider)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.items > .item > .content",): [
 | 
			
		||||
            ("color", "var(--text-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.items > .item .extra",): [
 | 
			
		||||
            ("color", "var(--really-discrete-text-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			@ -503,8 +534,12 @@ REPLACEMENTS = {
 | 
			
		|||
            ".black",
 | 
			
		||||
            ".pink",
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.header",): [("color", "var(--header-color)"),],
 | 
			
		||||
        (".ui.header .sub.header",): [("color", "var(--header-color)"),],
 | 
			
		||||
        (".ui.header",): [
 | 
			
		||||
            ("color", "var(--header-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.header .sub.header",): [
 | 
			
		||||
            ("color", "var(--header-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "label": {
 | 
			
		||||
        "skip": [
 | 
			
		||||
| 
						 | 
				
			
			@ -580,7 +615,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ".danger",
 | 
			
		||||
            ".elastic",
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.inverted.dimmer > .ui.loader",): [("color", "var(--dimmer-color)"),],
 | 
			
		||||
        (".ui.inverted.dimmer > .ui.loader",): [
 | 
			
		||||
            ("color", "var(--dimmer-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "message": {
 | 
			
		||||
        "skip": [
 | 
			
		||||
| 
						 | 
				
			
			@ -620,7 +657,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ".fitted",
 | 
			
		||||
            "fixed",
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.menu .item",): [("color", "var(--menu-item-color)"),],
 | 
			
		||||
        (".ui.menu .item",): [
 | 
			
		||||
            ("color", "var(--menu-item-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.vertical.inverted.menu .menu .item", ".ui.inverted.menu .item"): [
 | 
			
		||||
            ("color", "var(--inverted-menu-item-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			@ -633,7 +672,9 @@ REPLACEMENTS = {
 | 
			
		|||
        (
 | 
			
		||||
            ".ui.secondary.pointing.menu a.item:hover",
 | 
			
		||||
            ".ui.secondary.pointing.menu .active.item:hover",
 | 
			
		||||
        ): [("color", "var(--secondary-menu-hover-item-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--secondary-menu-hover-item-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.menu .ui.dropdown .menu > .item",): [
 | 
			
		||||
            ("color", "var(--dropdown-item-color) !important"),
 | 
			
		||||
        ],
 | 
			
		||||
| 
						 | 
				
			
			@ -656,7 +697,9 @@ REPLACEMENTS = {
 | 
			
		|||
            ("border-bottom", "var(--divider)"),
 | 
			
		||||
            ("border-top", "var(--divider)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.modal > .close.inside",): [("color", "var(--text-color)"),],
 | 
			
		||||
        (".ui.modal > .close.inside",): [
 | 
			
		||||
            ("color", "var(--text-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.modal > .header",): [
 | 
			
		||||
            ("color", "var(--header-color)"),
 | 
			
		||||
            ("background", "var(--modal-background)"),
 | 
			
		||||
| 
						 | 
				
			
			@ -680,7 +723,9 @@ REPLACEMENTS = {
 | 
			
		|||
        (
 | 
			
		||||
            ".ui.search > .results .result .title",
 | 
			
		||||
            ".ui.search > .results .result .description",
 | 
			
		||||
        ): [("color", "var(--dropdown-item-color)"),],
 | 
			
		||||
        ): [
 | 
			
		||||
            ("color", "var(--dropdown-item-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
        (".ui.search > .results .result:hover",): [
 | 
			
		||||
            ("color", "var(--dropdown-item-hover-color)"),
 | 
			
		||||
            ("background", "var(--dropdown-item-hover-background)"),
 | 
			
		||||
| 
						 | 
				
			
			@ -696,7 +741,9 @@ REPLACEMENTS = {
 | 
			
		|||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "sidebar": {
 | 
			
		||||
        (".ui.left.visible.sidebar",): [("box-shadow", "var(--sidebar-box-shadow)"),]
 | 
			
		||||
        (".ui.left.visible.sidebar",): [
 | 
			
		||||
            ("box-shadow", "var(--sidebar-box-shadow)"),
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "statistic": {
 | 
			
		||||
        (".ui.statistic > .value", ".ui.statistic > .label"): [
 | 
			
		||||
| 
						 | 
				
			
			@ -704,7 +751,9 @@ REPLACEMENTS = {
 | 
			
		|||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "progress": {
 | 
			
		||||
        (".ui.progress.success > .label",): [("color", "var(--text-color)"),],
 | 
			
		||||
        (".ui.progress.success > .label",): [
 | 
			
		||||
            ("color", "var(--text-color)"),
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    "table": {
 | 
			
		||||
        "skip": [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,4 +43,4 @@
 | 
			
		|||
        showcontent = true
 | 
			
		||||
 | 
			
		||||
[tool.black]
 | 
			
		||||
    exclude = "(.git|.hg|.mypy_cache|.tox|.venv|_build|buck-out|build|dist|migrations)"
 | 
			
		||||
    exclude = "(.git|.hg|.mypy_cache|.tox|.venv|_build|buck-out|build|dist|migrations|data)"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue