diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index eec7a02cc301664901d37adbe8143d3b18e01faf..15d70d5b4cd61283f01e0924d043ed54964344dc 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -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}) diff --git a/api/funkwhale_api/federation/management/__init__.py b/api/funkwhale_api/federation/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/funkwhale_api/federation/management/commands/fix_federation_ids.py b/api/funkwhale_api/federation/management/commands/fix_federation_ids.py new file mode 100644 index 0000000000000000000000000000000000000000..e53eb62b43db818c6996c738f5292f4e90ba8527 --- /dev/null +++ b/api/funkwhale_api/federation/management/commands/fix_federation_ids.py @@ -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!")) diff --git a/api/tests/common/test_utils.py b/api/tests/common/test_utils.py index 28d757eb56f98f70716a57b938742d4cab6f8738..c5c4a8e239b9e24f1699b3d29b36e2d0b720e0f8 100644 --- a/api/tests/common/test_utils.py +++ b/api/tests/common/test_utils.py @@ -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://") diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py new file mode 100644 index 0000000000000000000000000000000000000000..621b8cf64da8d175a4d2ac252d6478dbd72d0863 --- /dev/null +++ b/api/tests/federation/test_commands.py @@ -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" + ) diff --git a/changes/changelog.d/706.enhancement b/changes/changelog.d/706.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..7ede46b983b75cdfb92d910f865b2d9726b18198 --- /dev/null +++ b/changes/changelog.d/706.enhancement @@ -0,0 +1,2 @@ +Added a 'fix_federation_ids' management command to deal with protocol/domain issues in federation +IDs after deployments (#706) diff --git a/changes/changelog.d/707.bugfix b/changes/changelog.d/707.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..3cd05c9743e510dcbd674dc0654eb7e9bc2a0780 --- /dev/null +++ b/changes/changelog.d/707.bugfix @@ -0,0 +1 @@ +Fixed cards display issues on medium/small screens (#707) diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue index 7c3c540ca1b478f93675cc6e08e60b3d5e545a7e..cdf933a61242200c17368125f8bc09783fd28de8 100644 --- a/front/src/components/audio/Search.vue +++ b/front/src/components/audio/Search.vue @@ -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> diff --git a/front/src/components/federation/LibraryWidget.vue b/front/src/components/federation/LibraryWidget.vue index cf33b0add400791c0f7cedaa16fbf3acfc0025af..b9554d01ff36b235456dcf467647683ae769992b 100644 --- a/front/src/components/federation/LibraryWidget.vue +++ b/front/src/components/federation/LibraryWidget.vue @@ -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> diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue index ca7e2a5a03cdbc3df968f2043d133a6e1a107e82..41b3b2c6fbe080e23e7ffc661b4c5d8b28f6e4bb 100644 --- a/front/src/components/library/Artists.vue +++ b/front/src/components/library/Artists.vue @@ -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"> diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue index 7aec1d61b01ea1da0203861b1450d829fc3d01f8..5b433dcb2d3da801b819c7171ff5f3859b70268e 100644 --- a/front/src/components/library/Radios.vue +++ b/front/src/components/library/Radios.vue @@ -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"> diff --git a/front/src/components/playlists/CardList.vue b/front/src/components/playlists/CardList.vue index 4d4746090f757dddaf5d33cd4d924440e725ca09..44504a5738ad1d83ded1884a9ed454495f0bc52d 100644 --- a/front/src/components/playlists/CardList.vue +++ b/front/src/components/playlists/CardList.vue @@ -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> diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue index c289e6de9fafaa506a6398070beb251a0ba5ed50..7329c502ec27f0f1f55cf41abea6636c9cf53ea7 100644 --- a/front/src/components/playlists/Widget.vue +++ b/front/src/components/playlists/Widget.vue @@ -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> diff --git a/front/src/views/content/libraries/Card.vue b/front/src/views/content/libraries/Card.vue index 775cbf60433062b757efe906dc1581f6609eb3d8..b60c2c1b986c3f0643833862aacea9cb860b43b0 100644 --- a/front/src/views/content/libraries/Card.vue +++ b/front/src/views/content/libraries/Card.vue @@ -1,5 +1,5 @@ <template> - <div class="ui fluid card"> + <div class="ui card"> <div class="content"> <div class="header"> {{ library.name }} diff --git a/front/src/views/content/remote/Card.vue b/front/src/views/content/remote/Card.vue index ce5ab53eef0761889d04a04506515ddbf3f90ed9..f596572959c24999c42c51e641cfa4ddf1654b1e 100644 --- a/front/src/views/content/remote/Card.vue +++ b/front/src/views/content/remote/Card.vue @@ -1,5 +1,5 @@ <template> - <div class="ui fluid card"> + <div class="ui card"> <div class="content"> <div class="header"> {{ library.name }}