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