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