Commit 7e1bd1ad authored by Eliot Berriot's avatar Eliot Berriot 💬

Merge branch 'master' into develop

parents 0354edf4 db5b28ab
......@@ -168,3 +168,28 @@ def recursive_getattr(obj, key, permissive=False):
return
return v
def replace_prefix(queryset, field, old, new):
"""
Given a queryset of objects and a field name, will find objects
for which the field have the given value, and replace the old prefix by
the new one.
This is especially useful to find/update bad federation ids, to replace:
http://wrongprotocolanddomain/path
by
https://goodprotocalanddomain/path
on a whole table with a single query.
"""
qs = queryset.filter(**{"{}__startswith".format(field): old})
# we extract the part after the old prefix, and Concat it with our new prefix
update = models.functions.Concat(
models.Value(new),
models.functions.Substr(field, len(old) + 1, output_field=models.CharField()),
)
return qs.update(**{field: update})
from django.core.management.base import BaseCommand, CommandError
from funkwhale_api.common import utils
from funkwhale_api.federation import models as federation_models
from funkwhale_api.music import models as music_models
MODELS = [
(music_models.Artist, ["fid"]),
(music_models.Album, ["fid"]),
(music_models.Track, ["fid"]),
(music_models.Upload, ["fid"]),
(music_models.Library, ["fid", "followers_url"]),
(
federation_models.Actor,
[
"fid",
"url",
"outbox_url",
"inbox_url",
"following_url",
"followers_url",
"shared_inbox_url",
],
),
(federation_models.Activity, ["fid"]),
(federation_models.Follow, ["fid"]),
(federation_models.LibraryFollow, ["fid"]),
]
class Command(BaseCommand):
help = """
Find and replace wrong protocal/domain in local federation ids.
Use with caution and only if you know what you are doing.
"""
def add_arguments(self, parser):
parser.add_argument(
"old_base_url",
type=str,
help="The invalid url prefix you want to find and replace, e.g 'http://baddomain'",
)
parser.add_argument(
"new_base_url",
type=str,
help="The url prefix you want to use in place of the bad one, e.g 'https://gooddomain'",
)
parser.add_argument(
"--noinput",
"--no-input",
action="store_false",
dest="interactive",
help="Do NOT prompt the user for input of any kind.",
)
parser.add_argument(
"--no-dry-run",
action="store_false",
dest="dry_run",
help="Commit the changes to the database",
)
def handle(self, *args, **options):
results = {}
old_prefix, new_prefix = options["old_base_url"], options["new_base_url"]
for kls, fields in MODELS:
results[kls] = {}
for field in fields:
candidates = kls.objects.filter(
**{"{}__startswith".format(field): old_prefix}
)
results[kls][field] = candidates.count()
total = sum([t for k in results.values() for t in k.values()])
self.stdout.write("")
if total:
self.stdout.write(
self.style.WARNING(
"Will replace {} found occurences of '{}' by '{}':".format(
total, old_prefix, new_prefix
)
)
)
self.stdout.write("")
for kls, fields in results.items():
for field, count in fields.items():
self.stdout.write(
"- {}/{} {}.{}".format(
count, kls.objects.count(), kls._meta.label, field
)
)
else:
self.stdout.write(
"No objects found with prefix {}, exiting.".format(old_prefix)
)
return
if options["dry_run"]:
self.stdout.write(
"Run this command with --no-dry-run to perform the replacement."
)
return
self.stdout.write("")
if options["interactive"]:
message = (
"Are you sure you want to perform the replacement on {} objects?\n\n"
"Type 'yes' to continue, or 'no' to cancel: "
).format(total)
if input("".join(message)) != "yes":
raise CommandError("Command canceled.")
for kls, fields in results.items():
for field, count in fields.items():
self.stdout.write(
"Replacing {} on {} {}…".format(field, count, kls._meta.label)
)
candidates = kls.objects.all()
utils.replace_prefix(candidates, field, old=old_prefix, new=new_prefix)
self.stdout.write("")
self.stdout.write(self.style.SUCCESS("Done!"))
......@@ -8,3 +8,37 @@ def test_chunk_queryset(factories):
assert list(chunks[0]) == actors[0:2]
assert list(chunks[1]) == actors[2:4]
def test_update_prefix(factories):
actors = []
fid = "http://hello.world/actor/{}/"
for i in range(3):
actors.append(factories["federation.Actor"](fid=fid.format(i)))
noop = [
factories["federation.Actor"](fid="https://hello.world/actor/witness/"),
factories["federation.Actor"](fid="http://another.world/actor/witness/"),
factories["federation.Actor"](fid="http://foo.bar/actor/witness/"),
]
qs = actors[0].__class__.objects.filter(fid__startswith="http://hello.world")
assert qs.count() == 3
result = utils.replace_prefix(
actors[0].__class__.objects.all(),
"fid",
"http://hello.world",
"https://hello.world",
)
assert result == 3
for n in noop:
old = n.fid
n.refresh_from_db()
assert old == n.fid
for n in actors:
old = n.fid
n.refresh_from_db()
assert n.fid == old.replace("http://", "https://")
from django.core.management import call_command
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation.management.commands import fix_federation_ids
from funkwhale_api.music import models as music_models
def test_fix_fids_dry_run(factories, mocker):
replace_prefix = mocker.patch("funkwhale_api.common.utils.replace_prefix")
call_command("fix_federation_ids", "http://old/", "https://new/", interactive=False)
replace_prefix.assert_not_called()
def test_fix_fids_no_dry_run(factories, mocker, queryset_equal_queries):
replace_prefix = mocker.patch("funkwhale_api.common.utils.replace_prefix")
factories["federation.Actor"](fid="http://old/test")
call_command(
"fix_federation_ids",
"http://old",
"https://new",
interactive=False,
dry_run=False,
)
models = [
(music_models.Artist, ["fid"]),
(music_models.Album, ["fid"]),
(music_models.Track, ["fid"]),
(music_models.Upload, ["fid"]),
(music_models.Library, ["fid", "followers_url"]),
(
federation_models.Actor,
[
"fid",
"url",
"outbox_url",
"inbox_url",
"following_url",
"followers_url",
"shared_inbox_url",
],
),
(federation_models.Activity, ["fid"]),
(federation_models.Follow, ["fid"]),
(federation_models.LibraryFollow, ["fid"]),
]
assert models == fix_federation_ids.MODELS
for kls, fields in models:
for field in fields:
replace_prefix.assert_any_call(
kls.objects.all(), field, old="http://old", new="https://new"
)
Added a 'fix_federation_ids' management command to deal with protocol/domain issues in federation
IDs after deployments (#706)
Fixed cards display issues on medium/small screens (#707)
......@@ -9,9 +9,9 @@
</div>
<template v-if="query.length > 0">
<h3 class="ui title"><translate :translate-context="'Content/Search/Title'">Artists</translate></h3>
<div v-if="results.artists.length > 0" class="ui stackable three column grid">
<div class="column" :key="artist.id" v-for="artist in results.artists">
<artist-card class="fluid" :artist="artist" ></artist-card>
<div v-if="results.artists.length > 0">
<div class="ui cards">
<artist-card :key="artist.id" v-for="artist in results.artists" :artist="artist" ></artist-card>
</div>
</div>
<p v-else><translate :translate-context="'Content/Search/Paragraph'">No artist matched your query</translate></p>
......@@ -101,5 +101,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
......@@ -10,7 +10,7 @@
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'angle right', 'icon']">
</i>
<div class="ui hidden divider"></div>
<div class="ui three cards">
<div class="ui cards">
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
......
......@@ -42,17 +42,15 @@
v-if="result"
v-masonry
transition-duration="0"
item-selector=".column"
item-selector=".card"
percent-position="true"
stagger="0"
class="ui stackable three column doubling grid">
<div
v-masonry-tile
v-if="result.results.length > 0"
v-for="artist in result.results"
:key="artist.id"
class="column">
<artist-card class="fluid" :artist="artist"></artist-card>
stagger="0">
<div v-if="result.results.length > 0" class="ui cards">
<artist-card
v-masonry-tile
v-for="artist in result.results"
:key="artist.id"
:artist="artist"></artist-card>
</div>
</div>
<div class="ui center aligned basic segment">
......
......@@ -64,17 +64,18 @@
v-if="result"
v-masonry
transition-duration="0"
item-selector=".column"
item-selector=".card"
percent-position="true"
stagger="0"
class="ui stackable three column doubling grid">
stagger="0">
<div
v-masonry-tile
v-if="result.results.length > 0"
v-for="radio in result.results"
:key="radio.id"
class="column">
<radio-card class="fluid" type="custom" :custom-radio="radio"></radio-card>
class="ui cards">
<radio-card
type="custom"
v-masonry-tile
v-for="radio in result.results"
:key="radio.id"
:custom-radio="radio"></radio-card>
</div>
</div>
<div class="ui center aligned basic segment">
......
......@@ -3,16 +3,16 @@
v-if="playlists.length > 0"
v-masonry
transition-duration="0"
item-selector=".column"
item-selector=".card"
percent-position="true"
stagger="0"
class="ui stackable three column doubling grid">
<div
v-masonry-tile
v-for="playlist in playlists"
:key="playlist.id"
class="column">
<playlist-card class="fluid" :playlist="playlist"></playlist-card>
stagger="0">
<div class="ui cards">
<playlist-card
:playlist="playlist"
v-masonry-tile
v-for="playlist in playlists"
:key="playlist.id"
></playlist-card>
</div>
</div>
</template>
......
......@@ -10,7 +10,7 @@
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<playlist-card class="fluid" v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card>
<playlist-card v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card>
</div>
</template>
......
<template>
<div class="ui fluid card">
<div class="ui card">
<div class="content">
<div class="header">
{{ library.name }}
......
<template>
<div class="ui fluid card">
<div class="ui card">
<div class="content">
<div class="header">
{{ library.name }}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment