From ac7db737857c1a070574174db4b90450a380a331 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Fri, 18 May 2018 21:19:20 +0200 Subject: [PATCH] See #152: added management command to execute one-time migration scripts --- .../common/management/__init__.py | 0 .../common/management/commands/__init__.py | 0 .../common/management/commands/script.py | 66 +++++++++++++++++++ api/funkwhale_api/common/scripts/__init__.py | 2 + .../django_permissions_to_user_permissions.py | 29 ++++++++ api/funkwhale_api/common/scripts/test.py | 8 +++ api/funkwhale_api/users/admin.py | 17 ++++- api/funkwhale_api/users/factories.py | 28 +++++++- api/tests/common/test_scripts.py | 56 ++++++++++++++++ 9 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 api/funkwhale_api/common/management/__init__.py create mode 100644 api/funkwhale_api/common/management/commands/__init__.py create mode 100644 api/funkwhale_api/common/management/commands/script.py create mode 100644 api/funkwhale_api/common/scripts/__init__.py create mode 100644 api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py create mode 100644 api/funkwhale_api/common/scripts/test.py create mode 100644 api/tests/common/test_scripts.py diff --git a/api/funkwhale_api/common/management/__init__.py b/api/funkwhale_api/common/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/funkwhale_api/common/management/commands/__init__.py b/api/funkwhale_api/common/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/funkwhale_api/common/management/commands/script.py b/api/funkwhale_api/common/management/commands/script.py new file mode 100644 index 00000000..9d26a583 --- /dev/null +++ b/api/funkwhale_api/common/management/commands/script.py @@ -0,0 +1,66 @@ +from django.core.management.base import BaseCommand, CommandError + +from funkwhale_api.common import scripts + + +class Command(BaseCommand): + help = 'Run a specific script from funkwhale_api/common/scripts/' + + def add_arguments(self, parser): + parser.add_argument('script_name', nargs='?', type=str) + parser.add_argument( + '--noinput', '--no-input', action='store_false', dest='interactive', + help="Do NOT prompt the user for input of any kind.", + ) + + def handle(self, *args, **options): + name = options['script_name'] + if not name: + self.show_help() + + available_scripts = self.get_scripts() + try: + script = available_scripts[name] + except KeyError: + raise CommandError( + '{} is not a valid script. Run python manage.py script for a ' + 'list of available scripts'.format(name)) + + self.stdout.write('') + if options['interactive']: + message = ( + 'Are you sure you want to execute the script {}?\n\n' + "Type 'yes' to continue, or 'no' to cancel: " + ).format(name) + if input(''.join(message)) != 'yes': + raise CommandError("Script cancelled.") + script['entrypoint'](self, **options) + + def show_help(self): + indentation = 4 + self.stdout.write('') + self.stdout.write('Available scripts:') + self.stdout.write('Launch with: python manage.py <script_name>') + available_scripts = self.get_scripts() + for name, script in sorted(available_scripts.items()): + self.stdout.write('') + self.stdout.write(self.style.SUCCESS(name)) + self.stdout.write('') + for line in script['help'].splitlines(): + self.stdout.write('Â Â Â Â {}'.format(line)) + self.stdout.write('') + + def get_scripts(self): + available_scripts = [ + k for k in sorted(scripts.__dict__.keys()) + if not k.startswith('__') + ] + data = {} + for name in available_scripts: + module = getattr(scripts, name) + data[name] = { + 'name': name, + 'help': module.__doc__.strip(), + 'entrypoint': module.main + } + return data diff --git a/api/funkwhale_api/common/scripts/__init__.py b/api/funkwhale_api/common/scripts/__init__.py new file mode 100644 index 00000000..4b2d5252 --- /dev/null +++ b/api/funkwhale_api/common/scripts/__init__.py @@ -0,0 +1,2 @@ +from . import django_permissions_to_user_permissions +from . import test diff --git a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py new file mode 100644 index 00000000..1bc971f8 --- /dev/null +++ b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py @@ -0,0 +1,29 @@ +""" +Convert django permissions to user permissions in the database, +following the work done in #152. +""" +from django.db.models import Q +from funkwhale_api.users import models + +from django.contrib.auth.models import Permission + +mapping = { + 'dynamic_preferences.change_globalpreferencemodel': 'settings', + 'music.add_importbatch': 'library', + 'federation.change_library': 'federation', +} + + +def main(command, **kwargs): + for codename, user_permission in sorted(mapping.items()): + app_label, c = codename.split('.') + p = Permission.objects.get( + content_type__app_label=app_label, codename=c) + users = models.User.objects.filter( + Q(groups__permissions=p) | Q(user_permissions=p)).distinct() + total = users.count() + + command.stdout.write('Updating {} users with {} permission...'.format( + total, user_permission + )) + users.update(**{'permission_{}'.format(user_permission): True}) diff --git a/api/funkwhale_api/common/scripts/test.py b/api/funkwhale_api/common/scripts/test.py new file mode 100644 index 00000000..ab401dca --- /dev/null +++ b/api/funkwhale_api/common/scripts/test.py @@ -0,0 +1,8 @@ +""" +This is a test script that does nothing. +You can launch it just to check how it works. +""" + + +def main(command, **kwargs): + command.stdout.write('Test script run successfully') diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index c772603e..7e9062a1 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -57,7 +57,18 @@ class UserAdmin(AuthUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password', 'privacy_level')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', - 'permission_library', 'permission_settings', 'permission_federation')}), + (_('Permissions'), { + 'fields': ( + 'is_active', + 'is_staff', + 'is_superuser', + 'permission_library', + 'permission_settings', + 'permission_federation')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), - ) + (_('Useless fields'), { + 'fields': ( + 'user_permissions', + 'groups', + )}) + ) diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index 12307f7f..cd28f440 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -1,15 +1,41 @@ import factory -from funkwhale_api.factories import registry +from funkwhale_api.factories import registry, ManyToManyFromList from django.contrib.auth.models import Permission +@registry.register +class GroupFactory(factory.django.DjangoModelFactory): + name = factory.Sequence(lambda n: 'group-{0}'.format(n)) + + class Meta: + model = 'auth.Group' + + @factory.post_generation + def perms(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + perms = [ + Permission.objects.get( + content_type__app_label=p.split('.')[0], + codename=p.split('.')[1], + ) + for p in extracted + ] + # A list of permissions were passed in, use them + self.permissions.add(*perms) + + @registry.register class UserFactory(factory.django.DjangoModelFactory): username = factory.Sequence(lambda n: 'user-{0}'.format(n)) email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n)) password = factory.PostGenerationMethodCall('set_password', 'test') subsonic_api_token = None + groups = ManyToManyFromList('groups') class Meta: model = 'users.User' diff --git a/api/tests/common/test_scripts.py b/api/tests/common/test_scripts.py new file mode 100644 index 00000000..ce478ba0 --- /dev/null +++ b/api/tests/common/test_scripts.py @@ -0,0 +1,56 @@ +import pytest + +from funkwhale_api.common.management.commands import script +from funkwhale_api.common import scripts + + +@pytest.fixture +def command(): + return script.Command() + + +@pytest.mark.parametrize('script_name', [ + 'django_permissions_to_user_permissions', + 'test', +]) +def test_script_command_list(command, script_name, mocker): + mocked = mocker.patch( + 'funkwhale_api.common.scripts.{}.main'.format(script_name)) + + command.handle(script_name=script_name, interactive=False) + + mocked.assert_called_once_with( + command, script_name=script_name, interactive=False) + + +def test_django_permissions_to_user_permissions(factories, command): + group = factories['auth.Group']( + perms=[ + 'federation.change_library' + ] + ) + user1 = factories['users.User']( + perms=[ + 'dynamic_preferences.change_globalpreferencemodel', + 'music.add_importbatch', + ] + ) + user2 = factories['users.User']( + perms=[ + 'music.add_importbatch', + ], + groups=[group] + ) + + scripts.django_permissions_to_user_permissions.main(command) + + user1.refresh_from_db() + user2.refresh_from_db() + + assert user1.permission_settings is True + assert user1.permission_library is True + assert user1.permission_federation is False + + assert user2.permission_settings is False + assert user2.permission_library is True + assert user2.permission_federation is True -- GitLab