Merge branch 'master' into develop
This commit is contained in:
commit
7e1bd1ad07
|
@ -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})
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
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://")
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
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"
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
Added a 'fix_federation_ids' management command to deal with protocol/domain issues in federation
|
||||
IDs after deployments (#706)
|
|
@ -0,0 +1 @@
|
|||
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>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="ui fluid card">
|
||||
<div class="ui card">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
{{ library.name }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="ui fluid card">
|
||||
<div class="ui card">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
{{ library.name }}
|
||||
|
|
Loading…
Reference in New Issue