Merge branch '212-user-management-interface' into 'develop'

Resolve "Implement a user management interface"

Closes #212

See merge request funkwhale/funkwhale!262
This commit is contained in:
Eliot Berriot 2018-06-19 18:57:04 +00:00
commit 33b6db8dc1
22 changed files with 711 additions and 7 deletions

View File

@ -20,7 +20,7 @@ black:
before_script: before_script:
- pip install black - pip install black
script: script:
- black --check --diff api/ - black --exclude "/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|migrations)/" --check --diff api/
flake8: flake8:
image: python:3.6 image: python:3.6

View File

@ -146,6 +146,7 @@ MIDDLEWARE = (
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"funkwhale_api.users.middleware.RecordActivityMiddleware",
) )
# MIGRATIONS CONFIGURATION # MIGRATIONS CONFIGURATION

View File

@ -3,6 +3,7 @@ from django_filters import rest_framework as filters
from funkwhale_api.common import fields from funkwhale_api.common import fields
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from funkwhale_api.users import models as users_models
class ManageTrackFileFilterSet(filters.FilterSet): class ManageTrackFileFilterSet(filters.FilterSet):
@ -18,3 +19,21 @@ class ManageTrackFileFilterSet(filters.FilterSet):
class Meta: class Meta:
model = music_models.TrackFile model = music_models.TrackFile
fields = ["q", "track__album", "track__artist", "track", "library_track"] fields = ["q", "track__album", "track__artist", "track", "library_track"]
class ManageUserFilterSet(filters.FilterSet):
q = fields.SearchFilter(search_fields=["username", "email", "name"])
class Meta:
model = users_models.User
fields = [
"q",
"is_active",
"privacy_level",
"is_staff",
"is_superuser",
"permission_upload",
"permission_library",
"permission_settings",
"permission_federation",
]

View File

@ -3,6 +3,7 @@ from rest_framework import serializers
from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from funkwhale_api.users import models as users_models
from . import filters from . import filters
@ -67,3 +68,50 @@ class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
@transaction.atomic @transaction.atomic
def handle_delete(self, objects): def handle_delete(self, objects):
return objects.delete() 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 = PermissionsSerializer(source="*")
class Meta:
model = users_models.User
fields = (
"id",
"username",
"email",
"name",
"is_active",
"is_staff",
"is_superuser",
"date_joined",
"last_activity",
"permissions",
"privacy_level",
)
read_only_fields = [
"id",
"email",
"privacy_level",
"username",
"date_joined",
"last_activity",
]
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

View File

@ -5,7 +5,10 @@ from . import views
library_router = routers.SimpleRouter() library_router = routers.SimpleRouter()
library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files") library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files")
users_router = routers.SimpleRouter()
users_router.register(r"users", views.ManageUserViewSet, "users")
urlpatterns = [ urlpatterns = [
url(r"^library/", include((library_router.urls, "instance"), namespace="library")) url(r"^library/", include((library_router.urls, "instance"), namespace="library")),
url(r"^users/", include((users_router.urls, "instance"), namespace="users")),
] ]

View File

@ -1,7 +1,9 @@
from rest_framework import mixins, response, viewsets from rest_framework import mixins, response, viewsets
from rest_framework.decorators import list_route from rest_framework.decorators import list_route
from funkwhale_api.common import preferences
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from funkwhale_api.users import models as users_models
from funkwhale_api.users.permissions import HasUserPermission from funkwhale_api.users.permissions import HasUserPermission
from . import filters, serializers from . import filters, serializers
@ -41,3 +43,22 @@ class ManageTrackFileViewSet(
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
result = serializer.save() result = serializer.save()
return response.Response(result, status=200) return response.Response(result, status=200)
class ManageUserViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
queryset = users_models.User.objects.all().order_by("-id")
serializer_class = serializers.ManageUserSerializer
filter_class = filters.ManageUserFilterSet
permission_classes = (HasUserPermission,)
required_permissions = ["settings"]
ordering_fields = ["date_joined", "last_activity", "username"]
def get_serializer_context(self):
context = super().get_serializer_context()
context["default_permissions"] = preferences.get("users__default_permissions")
return context

View File

@ -0,0 +1,9 @@
class RecordActivityMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if hasattr(request, "user") and request.user.is_authenticated:
request.user.record_activity()
return response

View File

@ -0,0 +1,23 @@
# Generated by Django 2.0.6 on 2018-06-17 15:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0007_auto_20180524_2009'),
]
operations = [
migrations.AddField(
model_name='user',
name='last_activity',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='user',
name='permission_library',
field=models.BooleanField(default=False, help_text='Manage library, delete files, tracks, artists, albums...', verbose_name='Manage library'),
),
]

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import binascii import binascii
import datetime
import os import os
import uuid import uuid
@ -9,6 +10,7 @@ from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -75,11 +77,13 @@ class User(AbstractUser):
default=False, default=False,
) )
last_activity = models.DateTimeField(default=None, null=True, blank=True)
def __str__(self): def __str__(self):
return self.username return self.username
def get_permissions(self): def get_permissions(self, defaults=None):
defaults = preferences.get("users__default_permissions") defaults = defaults or preferences.get("users__default_permissions")
perms = {} perms = {}
for p in PERMISSIONS: for p in PERMISSIONS:
v = ( v = (
@ -90,6 +94,10 @@ class User(AbstractUser):
perms[p] = v perms[p] = v
return perms return perms
@property
def all_permissions(self):
return self.get_permissions()
def has_permissions(self, *perms, **kwargs): def has_permissions(self, *perms, **kwargs):
operator = kwargs.pop("operator", "and") operator = kwargs.pop("operator", "and")
if operator not in ["and", "or"]: if operator not in ["and", "or"]:
@ -117,3 +125,16 @@ class User(AbstractUser):
def get_activity_url(self): def get_activity_url(self):
return settings.FUNKWHALE_URL + "/@{}".format(self.username) return settings.FUNKWHALE_URL + "/@{}".format(self.username)
def record_activity(self):
"""
Simply update the last_activity field if current value is too old
than a threshold. This is useful to keep a track of inactive accounts.
"""
current = self.last_activity
delay = 60 * 15 # fifteen minutes
now = timezone.now()
if current is None or current < now - datetime.timedelta(seconds=delay):
self.last_activity = now
self.save(update_fields=["last_activity"])

View File

@ -7,6 +7,7 @@ import pytest
import requests_mock import requests_mock
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from django.utils import timezone
from django.test import client from django.test import client
from dynamic_preferences.registries import global_preferences_registry from dynamic_preferences.registries import global_preferences_registry
from rest_framework import fields as rest_fields from rest_framework import fields as rest_fields
@ -250,3 +251,10 @@ def to_api_date():
raise ValueError("Invalid value: {}".format(value)) raise ValueError("Invalid value: {}".format(value))
return inner return inner
@pytest.fixture()
def now(mocker):
now = timezone.now()
mocker.patch("django.utils.timezone.now", return_value=now)
return now

View File

@ -8,3 +8,26 @@ def test_manage_track_file_action_delete(factories):
s.handle_delete(tfs.__class__.objects.all()) s.handle_delete(tfs.__class__.objects.all())
assert tfs.__class__.objects.count() == 0 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

View File

@ -5,7 +5,11 @@ from funkwhale_api.manage import serializers, views
@pytest.mark.parametrize( @pytest.mark.parametrize(
"view,permissions,operator", [(views.ManageTrackFileViewSet, ["library"], "and")] "view,permissions,operator",
[
(views.ManageTrackFileViewSet, ["library"], "and"),
(views.ManageUserViewSet, ["settings"], "and"),
],
) )
def test_permissions(assert_user_permission, view, permissions, operator): def test_permissions(assert_user_permission, view, permissions, operator):
assert_user_permission(view, permissions, operator) assert_user_permission(view, permissions, operator)
@ -23,3 +27,18 @@ def test_track_file_view(factories, superuser_api_client):
assert response.data["count"] == len(tfs) assert response.data["count"] == len(tfs)
assert response.data["results"] == expected assert response.data["results"] == expected
def test_user_view(factories, superuser_api_client, mocker):
mocker.patch("funkwhale_api.users.models.User.record_activity")
users = factories["users.User"].create_batch(size=5) + [superuser_api_client.user]
qs = users[0].__class__.objects.order_by("-id")
url = reverse("api:v1:manage:users:users-list")
response = superuser_api_client.get(url, {"sort": "-id"})
expected = serializers.ManageUserSerializer(
qs, many=True, context={"request": response.wsgi_request}
).data
assert response.data["count"] == len(users)
assert response.data["results"] == expected

View File

@ -0,0 +1,18 @@
from funkwhale_api.users import middleware
def test_record_activity_middleware(factories, api_request, mocker):
m = middleware.RecordActivityMiddleware(lambda request: None)
user = factories["users.User"]()
record_activity = mocker.patch("funkwhale_api.users.models.User.record_activity")
request = api_request.get("/")
request.user = user
m(request)
record_activity.assert_called_once_with()
def test_record_activity_middleware_no_user(api_request):
m = middleware.RecordActivityMiddleware(lambda request: None)
request = api_request.get("/")
m(request)

View File

@ -78,3 +78,20 @@ def test_has_permissions_and(args, perms, expected, factories):
def test_has_permissions_or(args, perms, expected, factories): def test_has_permissions_or(args, perms, expected, factories):
user = factories["users.User"](**args) user = factories["users.User"](**args)
assert user.has_permissions(*perms, operator="or") is expected assert user.has_permissions(*perms, operator="or") is expected
def test_record_activity(factories, now):
user = factories["users.User"]()
assert user.last_activity is None
user.record_activity()
assert user.last_activity == now
def test_record_activity_does_nothing_if_already(factories, now, mocker):
user = factories["users.User"](last_activity=now)
save = mocker.patch("funkwhale_api.users.models.User.save")
user.record_activity()
save.assert_not_called()

View File

@ -0,0 +1 @@
Management interface for users (#212)

View File

@ -96,6 +96,12 @@
:to="{path: '/manage/settings'}"> :to="{path: '/manage/settings'}">
<i class="settings icon"></i>{{ $t('Settings') }} <i class="settings icon"></i>{{ $t('Settings') }}
</router-link> </router-link>
<router-link
class="item"
v-if="$store.state.auth.availablePermissions['settings']"
:to="{path: '/manage/users'}">
<i class="users icon"></i>{{ $t('Users') }}
</router-link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -61,7 +61,7 @@
</th> </th>
</tr> </tr>
<tr> <tr>
<th> <th v-if="actions.length > 0">
<div class="ui checkbox"> <div class="ui checkbox">
<input <input
type="checkbox" type="checkbox"
@ -75,7 +75,7 @@
</thead> </thead>
<tbody v-if="objectsData.count > 0"> <tbody v-if="objectsData.count > 0">
<tr v-for="(obj, index) in objectsData.results"> <tr v-for="(obj, index) in objectsData.results">
<td class="collapsing"> <td v-if="actions.length > 0" class="collapsing">
<input <input
type="checkbox" type="checkbox"
:disabled="checkable.indexOf(obj.id) === -1" :disabled="checkable.indexOf(obj.id) === -1"
@ -184,6 +184,9 @@ export default {
})[0] })[0]
}, },
checkable () { checkable () {
if (!this.currentAction) {
return []
}
let objs = this.objectsData.results let objs = this.objectsData.results
let filter = this.currentAction.filterCheckable let filter = this.currentAction.filterCheckable
if (filter) { if (filter) {

View File

@ -0,0 +1,216 @@
<template>
<div>
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by username, email, name..." />
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
</option>
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">{{ $t('Ascending') }}</option>
<option value="-">{{ $t('Descending') }}</option>
</select>
</div>
</div>
</div>
<div class="dimmable">
<div v-if="isLoading" class="ui active inverted dimmer">
<div class="ui loader"></div>
</div>
<action-table
v-if="result"
@action-launched="fetchData"
:objects-data="result"
:actions="actions"
:action-url="'manage/library/track-files/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Username') }}</th>
<th>{{ $t('Email') }}</th>
<th>{{ $t('Account status') }}</th>
<th>{{ $t('Sign-up') }}</th>
<th>{{ $t('Last activity') }}</th>
<th>{{ $t('Permissions') }}</th>
<th>{{ $t('Status') }}</th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<router-link :to="{name: 'manage.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.username }}</router-link>
</td>
<td>
<span>{{ scope.obj.email }}</span>
</td>
<td>
<span v-if="scope.obj.is_active" class="ui basic green label">{{ $t('Active') }}</span>
<span v-else class="ui basic grey label">{{ $t('Inactive') }}</span>
</td>
<td>
<human-date :date="scope.obj.date_joined"></human-date>
</td>
<td>
<human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date>
<template v-else>{{ $t('N/A') }}</template>
</td>
<td>
<template v-for="p in permissions">
<span class="ui basic tiny label" v-if="scope.obj.permissions[p.code]">{{ p.label }}</span>
</template>
</td>
<td>
<span v-if="scope.obj.is_superuser" class="ui pink label">{{ $t('Admin') }}</span>
<span v-else-if="scope.obj.is_staff" class="ui purple label">{{ $t('Staff member') }}</span>
<span v-else class="ui basic label">{{ $t('regular user') }}</span>
</td>
</template>
</action-table>
</div>
<div>
<pagination
v-if="result && result.results.length > 0"
@page-changed="selectPage"
:compact="true"
:current="page"
:paginate-by="paginateBy"
:total="result.count"
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
</span>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
import time from '@/utils/time'
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'
import OrderingMixin from '@/components/mixins/Ordering'
export default {
mixins: [OrderingMixin],
props: {
filters: {type: Object, required: false}
},
components: {
Pagination,
ActionTable
},
data () {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-date_joined')
return {
time,
isLoading: false,
result: null,
page: 1,
paginateBy: 50,
search: '',
orderingDirection: defaultOrdering.direction || '+',
ordering: defaultOrdering.field,
orderingOptions: [
['date_joined', 'Sign-up date'],
['last_activity', 'Last activity'],
['username', 'Username']
]
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
let params = _.merge({
'page': this.page,
'page_size': this.paginateBy,
'q': this.search,
'ordering': this.getOrderingAsString()
}, this.filters)
let self = this
self.isLoading = true
self.checked = []
axios.get('/manage/users/users/', {params: params}).then((response) => {
self.result = response.data
self.isLoading = false
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
},
selectPage: function (page) {
this.page = page
}
},
computed: {
privacyLevels () {
return {}
},
permissions () {
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')
}
]
},
actionFilters () {
var currentFilters = {
q: this.search
}
if (this.filters) {
return _.merge(currentFilters, this.filters)
} else {
return currentFilters
}
},
actions () {
return [
// {
// name: 'delete',
// label: this.$t('Delete'),
// isDangerous: true
// }
]
}
},
watch: {
search (newValue) {
this.page = 1
this.fetchData()
},
page () {
this.fetchData()
},
ordering () {
this.fetchData()
},
orderingDirection () {
this.fetchData()
}
}
}
</script>

View File

@ -31,6 +31,9 @@ import Favorites from '@/components/favorites/List'
import AdminSettings from '@/views/admin/Settings' import AdminSettings from '@/views/admin/Settings'
import AdminLibraryBase from '@/views/admin/library/Base' import AdminLibraryBase from '@/views/admin/library/Base'
import AdminLibraryFilesList from '@/views/admin/library/FilesList' 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 FederationBase from '@/views/federation/Base'
import FederationScan from '@/views/federation/Scan' import FederationScan from '@/views/federation/Scan'
import FederationLibraryDetail from '@/views/federation/LibraryDetail' import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@ -180,6 +183,23 @@ export default new Router({
} }
] ]
}, },
{
path: '/manage/users',
component: AdminUsersBase,
children: [
{
path: '',
name: 'manage.users.list',
component: AdminUsersList
},
{
path: ':id',
name: 'manage.users.detail',
component: AdminUsersDetail,
props: true
}
]
},
{ {
path: '/library', path: '/library',
component: Library, component: Library,

View File

@ -0,0 +1,28 @@
<template>
<div class="main pusher" v-title="$t('Manage users')">
<div class="ui secondary pointing menu">
<router-link
class="ui item"
:to="{name: 'manage.users.list'}">{{ $t('Users') }}</router-link>
</div>
<router-view :key="$route.fullPath"></router-view>
</div>
</template>
<script>
export default {}
</script>
<style lang="scss">
@import '../../../style/vendor/media';
.main.pusher > .ui.secondary.menu {
@include media(">tablet") {
margin: 0 2.5rem;
}
.item {
padding-top: 1.5em;
padding-bottom: 1.5em;
}
}
</style>

View File

@ -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>

View File

@ -0,0 +1,23 @@
<template>
<div v-title="$t('Users')">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Users') }}</h2>
<div class="ui hidden divider"></div>
<users-table></users-table>
</div>
</div>
</template>
<script>
import UsersTable from '@/components/manage/users/UsersTable'
export default {
components: {
UsersTable
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>