From 8636b456a8660eef30fe688a02a903a498f27f10 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 19 Jun 2018 20:11:40 +0200
Subject: [PATCH] See #212: user detail profile

---
 api/funkwhale_api/manage/serializers.py       |  22 ++-
 api/funkwhale_api/users/models.py             |   4 +
 api/tests/manage/test_serializers.py          |  23 +++
 .../components/manage/users/UsersTable.vue    |   2 +-
 front/src/router/index.js                     |   7 +
 front/src/views/admin/users/UsersDetail.vue   | 177 ++++++++++++++++++
 6 files changed, 231 insertions(+), 4 deletions(-)
 create mode 100644 front/src/views/admin/users/UsersDetail.vue

diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index 13f886a7..6e57db81 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -70,8 +70,16 @@ class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
         return objects.delete()
 
 
+class PermissionsSerializer(serializers.Serializer):
+    def to_representation(self, o):
+        return o.get_permissions(defaults=self.context.get("default_permissions"))
+
+    def to_internal_value(self, o):
+        return {"permissions": o}
+
+
 class ManageUserSerializer(serializers.ModelSerializer):
-    permissions = serializers.SerializerMethodField()
+    permissions = PermissionsSerializer(source="*")
 
     class Meta:
         model = users_models.User
@@ -97,5 +105,13 @@ class ManageUserSerializer(serializers.ModelSerializer):
             "last_activity",
         ]
 
-    def get_permissions(self, o):
-        return o.get_permissions(defaults=self.context.get("default_permissions"))
+    def update(self, instance, validated_data):
+        instance = super().update(instance, validated_data)
+        permissions = validated_data.pop("permissions", {})
+        if permissions:
+            for p, value in permissions.items():
+                setattr(instance, "permission_{}".format(p), value)
+            instance.save(
+                update_fields=["permission_{}".format(p) for p in permissions.keys()]
+            )
+        return instance
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 055a971b..15d16db2 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -94,6 +94,10 @@ class User(AbstractUser):
             perms[p] = v
         return perms
 
+    @property
+    def all_permissions(self):
+        return self.get_permissions()
+
     def has_permissions(self, *perms, **kwargs):
         operator = kwargs.pop("operator", "and")
         if operator not in ["and", "or"]:
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 893cfd86..2f0c6bc2 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -8,3 +8,26 @@ def test_manage_track_file_action_delete(factories):
     s.handle_delete(tfs.__class__.objects.all())
 
     assert tfs.__class__.objects.count() == 0
+
+
+def test_user_update_permission(factories):
+    user = factories["users.User"](
+        permission_library=False,
+        permission_upload=False,
+        permission_federation=True,
+        permission_settings=True,
+        is_active=True,
+    )
+    s = serializers.ManageUserSerializer(
+        user,
+        data={"is_active": False, "permissions": {"federation": False, "upload": True}},
+    )
+    s.is_valid(raise_exception=True)
+    s.save()
+    user.refresh_from_db()
+
+    assert user.is_active is False
+    assert user.permission_federation is False
+    assert user.permission_upload is True
+    assert user.permission_library is False
+    assert user.permission_settings is True
diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue
index 746b158a..5658583c 100644
--- a/front/src/components/manage/users/UsersTable.vue
+++ b/front/src/components/manage/users/UsersTable.vue
@@ -45,7 +45,7 @@
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
-            <span>{{ scope.obj.username }}</span>
+            <router-link :to="{name: 'manage.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.username }}</router-link>
           </td>
           <td>
             <span>{{ scope.obj.email }}</span>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 459077d3..0d2ad34f 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -32,6 +32,7 @@ import AdminSettings from '@/views/admin/Settings'
 import AdminLibraryBase from '@/views/admin/library/Base'
 import AdminLibraryFilesList from '@/views/admin/library/FilesList'
 import AdminUsersBase from '@/views/admin/users/Base'
+import AdminUsersDetail from '@/views/admin/users/UsersDetail'
 import AdminUsersList from '@/views/admin/users/UsersList'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
@@ -190,6 +191,12 @@ export default new Router({
           path: '',
           name: 'manage.users.list',
           component: AdminUsersList
+        },
+        {
+          path: ':id',
+          name: 'manage.users.detail',
+          component: AdminUsersDetail,
+          props: true
         }
       ]
     },
diff --git a/front/src/views/admin/users/UsersDetail.vue b/front/src/views/admin/users/UsersDetail.vue
new file mode 100644
index 00000000..ea92716c
--- /dev/null
+++ b/front/src/views/admin/users/UsersDetail.vue
@@ -0,0 +1,177 @@
+<template>
+  <div>
+    <div v-if="isLoading" class="ui vertical segment">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="object.username">
+        <div class="segment-content">
+          <h2 class="ui center aligned icon header">
+            <i class="circular inverted user red icon"></i>
+            <div class="content">
+              @{{ object.username }}
+            </div>
+          </h2>
+        </div>
+        <div class="ui hidden divider"></div>
+        <div class="ui one column centered grid">
+          <table class="ui collapsing very basic table">
+            <tbody>
+              <tr>
+                <td>
+                  {{ $t('Name') }}
+                </td>
+                <td>
+                  {{ object.name }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Email address') }}
+                </td>
+                <td>
+                  {{ object.email }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Sign-up') }}
+                </td>
+                <td>
+                  <human-date :date="object.date_joined"></human-date>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Last activity') }}
+                </td>
+                <td>
+                  <human-date v-if="object.last_activity" :date="object.last_activity"></human-date>
+                  <template v-else>{{ $t('N/A') }}</template>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Account active') }}
+                  <span :data-tooltip="$t('Determine if the user account is active or not. Inactive users cannot login or user the service.')"><i class="question circle icon"></i></span>
+                </td>
+                <td>
+                  <div class="ui toggle checkbox">
+                    <input
+                      @change="update('is_active')"
+                      v-model="object.is_active" type="checkbox">
+                    <label></label>
+                  </div>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Permissions') }}
+                </td>
+                <td>
+                  <select
+                    @change="update('permissions')"
+                    v-model="permissions"
+                    multiple
+                    class="ui search selection dropdown">
+                    <option v-for="p in allPermissions" :value="p.code">{{ p.label }}</option>
+                  </select>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <div class="ui hidden divider"></div>
+        <button @click="fetchData" class="ui basic button">{{ $t('Refresh') }}</button>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+
+import $ from 'jquery'
+import axios from 'axios'
+import logger from '@/logging'
+
+export default {
+  props: ['id'],
+  data () {
+    return {
+      isLoading: true,
+      object: null,
+      permissions: []
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      var self = this
+      this.isLoading = true
+      let url = 'manage/users/users/' + this.id + '/'
+      axios.get(url).then((response) => {
+        self.object = response.data
+        self.permissions = []
+        self.allPermissions.forEach(p => {
+          if (self.object.permissions[p.code]) {
+            self.permissions.push(p.code)
+          }
+        })
+        self.isLoading = false
+      })
+    },
+    update (attr) {
+      let newValue = this.object[attr]
+      let params = {}
+      if (attr === 'permissions') {
+        params['permissions'] = {}
+        this.allPermissions.forEach(p => {
+          params['permissions'][p.code] = this.permissions.indexOf(p.code) > -1
+        })
+      } else {
+        params[attr] = newValue
+      }
+      axios.patch('manage/users/users/' + this.id + '/', params).then((response) => {
+        logger.default.info(`${attr} was updated succcessfully to ${newValue}`)
+      }, (error) => {
+        logger.default.error(`Error while setting ${attr} to ${newValue}`, error)
+      })
+    }
+  },
+  computed: {
+    allPermissions () {
+      return [
+        {
+          'code': 'upload',
+          'label': this.$t('Upload')
+        },
+        {
+          'code': 'library',
+          'label': this.$t('Library')
+        },
+        {
+          'code': 'federation',
+          'label': this.$t('Federation')
+        },
+        {
+          'code': 'settings',
+          'label': this.$t('Settings')
+        }
+      ]
+    }
+  },
+  watch: {
+    object () {
+      this.$nextTick(() => {
+        $('select.dropdown').dropdown()
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
-- 
GitLab