mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 18:32:35 +00:00
Merge branch '419-admin-group-management' into 'develop'
Resolve "Admin group management" Closes #419 See merge request bramw/baserow!263
This commit is contained in:
commit
884002efb8
80 changed files with 1429 additions and 762 deletions
backend/src/baserow
changelog.mdpremium
backend
src/baserow_premium
admin
api
user_admin
tests/baserow_premium
admin
dashboard
groups
users
api/admin
dashboard
groups
users
web-frontend
modules/baserow_premium
adminTypes.js
assets/scss/components
components
crud_table
pages/admin
plugin.jsplugins.jsroutes.jsservices/admin
test
web-frontend/modules/core
|
@ -203,6 +203,7 @@ SPECTACULAR_SETTINGS = {
|
|||
{"name": "Database table grid view"},
|
||||
{"name": "Database table rows"},
|
||||
{"name": "Database tokens"},
|
||||
{"name": "Admin"},
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ def group_updated(sender, group, user, **kwargs):
|
|||
|
||||
|
||||
@receiver(signals.group_deleted)
|
||||
def group_deleted(sender, group_id, group, group_users, user, **kwargs):
|
||||
def group_deleted(sender, group_id, group, group_users, user=None, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_users.delay(
|
||||
[u.id for u in group_users],
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
* Made it possible to order the applications by drag and drop.
|
||||
* Made it possible to order the tables by drag and drop.
|
||||
* **Premium**: Added an admin dashboard.
|
||||
* **Premium**: Added group admin area allowing management of all baserow groups.
|
||||
* Added today, this month and this year filter.
|
||||
* Added a page containing external resources to the docs.
|
||||
* Added a human-readable error message when a user tries to sign in with a deactivated
|
||||
|
|
10
premium/backend/src/baserow_premium/admin/exceptions.py
Normal file
10
premium/backend/src/baserow_premium/admin/exceptions.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
class InvalidSortDirectionException(Exception):
|
||||
"""
|
||||
Raised when an invalid sort direction is provided.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSortAttributeException(Exception):
|
||||
"""
|
||||
Raised when a sort is requested for an invalid or non-existent field.
|
||||
"""
|
30
premium/backend/src/baserow_premium/admin/groups/handler.py
Normal file
30
premium/backend/src/baserow_premium/admin/groups/handler.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.signals import group_deleted
|
||||
from baserow.core.exceptions import IsNotAdminError
|
||||
|
||||
|
||||
class GroupsAdminHandler:
|
||||
def delete_group(self, user, group):
|
||||
"""
|
||||
Deletes an existing group and related applications if the user is staff.
|
||||
|
||||
:param user: The user on whose behalf the group is deleted
|
||||
:type: user: User
|
||||
:param group: The group instance that must be deleted.
|
||||
:type: group: Group
|
||||
:raises IsNotAdminError: If the user is not admin or staff.
|
||||
"""
|
||||
|
||||
if not user.is_staff:
|
||||
raise IsNotAdminError()
|
||||
|
||||
# Load the group users before the group is deleted so that we can pass those
|
||||
# along with the signal.
|
||||
group_id = group.id
|
||||
group_users = list(group.users.all())
|
||||
|
||||
CoreHandler()._delete_group(group)
|
||||
|
||||
group_deleted.send(
|
||||
self, group_id=group_id, group=group, group_users=group_users
|
||||
)
|
|
@ -14,15 +14,3 @@ class UserDoesNotExistException(Exception):
|
|||
"""
|
||||
Raised when a delete or update operation is attempted on an unknown user.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSortDirectionException(Exception):
|
||||
"""
|
||||
Raised when an invalid sort direction is provided.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSortAttributeException(Exception):
|
||||
"""
|
||||
Raised when a sort is requested for an invalid or non-existent field.
|
||||
"""
|
109
premium/backend/src/baserow_premium/admin/users/handler.py
Normal file
109
premium/backend/src/baserow_premium/admin/users/handler.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
from typing import Optional
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from baserow.core.exceptions import IsNotAdminError
|
||||
from baserow_premium.admin.users.exceptions import (
|
||||
CannotDeactivateYourselfException,
|
||||
CannotDeleteYourselfException,
|
||||
UserDoesNotExistException,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserAdminHandler:
|
||||
def update_user(
|
||||
self,
|
||||
requesting_user: User,
|
||||
user_id: int,
|
||||
username: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_staff: Optional[bool] = None,
|
||||
):
|
||||
"""
|
||||
Updates a specified user with new attribute values. Will raise an exception
|
||||
if a user attempts to de-activate or un-staff themselves.
|
||||
|
||||
:param requesting_user: The user who is making the request to update a user, the
|
||||
user must be a staff member or else an exception will
|
||||
be raised.
|
||||
:param user_id: The id of the user to update, if they do not exist raises a
|
||||
UserDoesNotExistException.
|
||||
:param is_staff: Optional value used to set if the user is an admin or not.
|
||||
:param is_active: Optional value to disable or enable login for the user.
|
||||
:param password: Optional new password to securely set for the user.
|
||||
:param name: Optional new name to set on the user.
|
||||
:param username: Optional new username/email to set for the user.
|
||||
"""
|
||||
|
||||
self._raise_if_not_permitted(requesting_user)
|
||||
self._raise_if_locking_self_out_of_admin(
|
||||
is_active, is_staff, requesting_user, user_id
|
||||
)
|
||||
|
||||
try:
|
||||
user = User.objects.select_for_update().get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise UserDoesNotExistException()
|
||||
|
||||
if is_staff is not None:
|
||||
user.is_staff = is_staff
|
||||
if is_active is not None:
|
||||
user.is_active = is_active
|
||||
if password is not None:
|
||||
user.set_password(password)
|
||||
if name is not None:
|
||||
user.first_name = name
|
||||
if username is not None:
|
||||
user.email = username
|
||||
user.username = username
|
||||
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _raise_if_locking_self_out_of_admin(
|
||||
is_active, is_staff, requesting_user, user_id
|
||||
):
|
||||
"""
|
||||
Raises an exception if the requesting_user is about to lock themselves out of
|
||||
the admin area of Baserow by either turning off their staff status or disabling
|
||||
their account.
|
||||
"""
|
||||
|
||||
is_setting_staff_to_false = is_staff is not None and not is_staff
|
||||
is_setting_active_to_false = is_active is not None and not is_active
|
||||
if user_id == requesting_user.id and (
|
||||
is_setting_staff_to_false or is_setting_active_to_false
|
||||
):
|
||||
raise CannotDeactivateYourselfException()
|
||||
|
||||
def delete_user(self, requesting_user: User, user_id: int):
|
||||
"""
|
||||
Deletes a specified user, raises an exception if you attempt to delete yourself.
|
||||
|
||||
:param requesting_user: The user who is making the delete request , the
|
||||
user must be a staff member or else an exception will
|
||||
be raised.
|
||||
:param user_id: The id of the user to update, if they do not exist raises a
|
||||
UnknownUserException.
|
||||
"""
|
||||
|
||||
self._raise_if_not_permitted(requesting_user)
|
||||
|
||||
if requesting_user.id == user_id:
|
||||
raise CannotDeleteYourselfException()
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
user.delete()
|
||||
except User.DoesNotExist:
|
||||
raise UserDoesNotExistException()
|
||||
|
||||
@staticmethod
|
||||
def _raise_if_not_permitted(requesting_user):
|
||||
if not requesting_user.is_staff:
|
||||
raise IsNotAdminError()
|
0
premium/backend/src/baserow_premium/api/__init__.py
Normal file
0
premium/backend/src/baserow_premium/api/__init__.py
Normal file
|
@ -1,9 +1,9 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from baserow_premium.api.admin_dashboard.views import AdminDashboardView
|
||||
from baserow_premium.api.admin.dashboard.views import AdminDashboardView
|
||||
|
||||
|
||||
app_name = "baserow_premium.api.admin_dashboard"
|
||||
app_name = "baserow_premium.api.admin.dashboard"
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", AdminDashboardView.as_view(), name="dashboard"),
|
|
@ -10,7 +10,8 @@ from rest_framework.views import APIView
|
|||
|
||||
from baserow.api.decorators import accept_timezone
|
||||
from baserow.core.models import Group, Application
|
||||
from baserow_premium.admin_dashboard.handler import AdminDashboardHandler
|
||||
|
||||
from baserow_premium.admin.dashboard.handler import AdminDashboardHandler
|
||||
|
||||
from .serializers import AdminDashboardSerializer
|
||||
|
||||
|
@ -22,16 +23,17 @@ class AdminDashboardView(APIView):
|
|||
permission_classes = (IsAdminUser,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin dashboard"],
|
||||
tags=["Admin"],
|
||||
operation_id="admin_dashboard",
|
||||
description="Returns the new and active users for the last 24 hours, 7 days and"
|
||||
" 30 days. The `previous_` values are the values of the period before, so for "
|
||||
"example `previous_new_users_last_24_hours` are the new users that signed up "
|
||||
"from 48 to 24 hours ago. It can be used to calculate an increase or decrease "
|
||||
"in the amount of signups. A list of the new and active users for every day "
|
||||
"for the last 30 days is also included.",
|
||||
"for the last 30 days is also included.\n\nThis is a **premium** feature.",
|
||||
responses={
|
||||
200: AdminDashboardSerializer,
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@accept_timezone()
|
13
premium/backend/src/baserow_premium/api/admin/errors.py
Normal file
13
premium/backend/src/baserow_premium/api/admin/errors.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION = (
|
||||
"ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"Attributes to sort by must be prefixed with one of '-' or '+'.",
|
||||
)
|
||||
ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE = (
|
||||
"ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"Invalid attribute name provided to sort by.",
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
||||
from baserow.core.models import Group, GroupUser
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class GroupAdminUsersSerializer(ModelSerializer):
|
||||
id = serializers.IntegerField(source="user.id")
|
||||
email = serializers.CharField(source="user.email")
|
||||
|
||||
class Meta:
|
||||
model = GroupUser
|
||||
|
||||
fields = ("id", "email", "permissions")
|
||||
|
||||
|
||||
class GroupsAdminResponseSerializer(ModelSerializer):
|
||||
users = GroupAdminUsersSerializer(source="groupuser_set", many=True)
|
||||
application_count = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"users",
|
||||
"application_count",
|
||||
"created_on",
|
||||
)
|
11
premium/backend/src/baserow_premium/api/admin/groups/urls.py
Normal file
11
premium/backend/src/baserow_premium/api/admin/groups/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from baserow_premium.api.admin.groups.views import GroupsAdminView, GroupAdminView
|
||||
|
||||
|
||||
app_name = "baserow_premium.api.admin.groups"
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", GroupsAdminView.as_view(), name="list"),
|
||||
url(r"^(?P<group_id>[0-9]+)/$", GroupAdminView.as_view(), name="edit"),
|
||||
]
|
|
@ -0,0 +1,87 @@
|
|||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import map_exceptions
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow.api.errors import ERROR_GROUP_DOES_NOT_EXIST
|
||||
from baserow.core.models import Group
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.exceptions import GroupDoesNotExist
|
||||
from baserow_premium.api.admin.views import AdminListingView
|
||||
from baserow_premium.admin.groups.handler import GroupsAdminHandler
|
||||
|
||||
from .serializers import GroupsAdminResponseSerializer
|
||||
|
||||
|
||||
class GroupsAdminView(AdminListingView):
|
||||
serializer_class = GroupsAdminResponseSerializer
|
||||
search_fields = ["id", "name"]
|
||||
sort_field_mapping = {
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
"application_count": "application_count",
|
||||
"created_on": "created_on",
|
||||
}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return Group.objects.prefetch_related(
|
||||
"groupuser_set", "groupuser_set__user"
|
||||
).annotate(application_count=Count("application"))
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin"],
|
||||
operation_id="admin_list_groups",
|
||||
description="Returns all groups with detailed information on each group, "
|
||||
"if the requesting user is staff.\n\nThis is a **premium** feature.",
|
||||
**AdminListingView.get_extend_schema_parameters(
|
||||
"groups", serializer_class, search_fields, sort_field_mapping
|
||||
),
|
||||
)
|
||||
def get(self, *args, **kwargs):
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class GroupAdminView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin"],
|
||||
operation_id="admin_delete_group",
|
||||
description="Deletes the specified group and the applications inside that "
|
||||
"group, if the requesting user is staff. \n\nThis is a **premium** feature.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="group_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The id of the group to delete",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
204: None,
|
||||
400: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
GroupDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def delete(self, request, group_id):
|
||||
"""Deletes the specified group"""
|
||||
|
||||
group = CoreHandler().get_group(
|
||||
group_id, base_queryset=Group.objects.select_for_update()
|
||||
)
|
||||
handler = GroupsAdminHandler()
|
||||
handler.delete_group(request.user, group)
|
||||
|
||||
return Response(status=204)
|
13
premium/backend/src/baserow_premium/api/admin/urls.py
Normal file
13
premium/backend/src/baserow_premium/api/admin/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from .users import urls as users_urls
|
||||
from .groups import urls as groups_urls
|
||||
from .dashboard import urls as dashboard_urls
|
||||
|
||||
app_name = "baserow_premium.api.admin"
|
||||
|
||||
urlpatterns = [
|
||||
path("dashboard/", include(dashboard_urls, namespace="dashboard")),
|
||||
path("users/", include(users_urls, namespace="users")),
|
||||
path("groups/", include(groups_urls, namespace="groups")),
|
||||
]
|
|
@ -1,18 +1,6 @@
|
|||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
USER_ADMIN_INVALID_SORT_DIRECTION = (
|
||||
"USER_ADMIN_INVALID_SORT_DIRECTION",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"Attributes to sort by must be prefixed with one of '-' or '+'.",
|
||||
)
|
||||
|
||||
USER_ADMIN_INVALID_SORT_ATTRIBUTE = (
|
||||
"USER_ADMIN_INVALID_SORT_ATTRIBUTE",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"Invalid attribute name provided to sort by.",
|
||||
)
|
||||
|
||||
USER_ADMIN_CANNOT_DEACTIVATE_SELF = (
|
||||
"USER_ADMIN_CANNOT_DEACTIVATE_SELF",
|
||||
HTTP_400_BAD_REQUEST,
|
|
@ -24,7 +24,7 @@ _USER_ADMIN_SERIALIZER_API_DOC_KWARGS = {
|
|||
}
|
||||
|
||||
|
||||
class AdminGroupUserSerializer(ModelSerializer):
|
||||
class UserAdminGroupsSerializer(ModelSerializer):
|
||||
id = serializers.IntegerField(source="group.id")
|
||||
name = serializers.CharField(source="group.name")
|
||||
|
||||
|
@ -46,7 +46,7 @@ class UserAdminResponseSerializer(ModelSerializer):
|
|||
# Max length set to match django user models first_name fields max length
|
||||
name = CharField(source="first_name", max_length=30)
|
||||
username = EmailField()
|
||||
groups = AdminGroupUserSerializer(source="groupuser_set", many=True)
|
||||
groups = UserAdminGroupsSerializer(source="groupuser_set", many=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
11
premium/backend/src/baserow_premium/api/admin/users/urls.py
Normal file
11
premium/backend/src/baserow_premium/api/admin/users/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from baserow_premium.api.admin.users.views import UsersAdminView, UserAdminView
|
||||
|
||||
|
||||
app_name = "baserow_premium.api.admin.users"
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", UsersAdminView.as_view(), name="list"),
|
||||
url(r"^(?P<user_id>[0-9]+)/$", UserAdminView.as_view(), name="edit"),
|
||||
]
|
158
premium/backend/src/baserow_premium/api/admin/users/views.py
Normal file
158
premium/backend/src/baserow_premium/api/admin/users/views.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
from django.db import transaction
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import validate_body, map_exceptions
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow_premium.api.admin.users.errors import (
|
||||
USER_ADMIN_CANNOT_DEACTIVATE_SELF,
|
||||
USER_ADMIN_CANNOT_DELETE_SELF,
|
||||
USER_ADMIN_UNKNOWN_USER,
|
||||
)
|
||||
from baserow_premium.api.admin.users.serializers import (
|
||||
UserAdminUpdateSerializer,
|
||||
UserAdminResponseSerializer,
|
||||
)
|
||||
from baserow_premium.admin.users.exceptions import (
|
||||
CannotDeactivateYourselfException,
|
||||
CannotDeleteYourselfException,
|
||||
UserDoesNotExistException,
|
||||
)
|
||||
from baserow_premium.admin.users.handler import UserAdminHandler
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from baserow_premium.api.admin.views import AdminListingView
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UsersAdminView(AdminListingView):
|
||||
serializer_class = UserAdminResponseSerializer
|
||||
search_fields = ["username"]
|
||||
sort_field_mapping = {
|
||||
"id": "id",
|
||||
"is_active": "is_active",
|
||||
"name": "first_name",
|
||||
"username": "username",
|
||||
"date_joined": "date_joined",
|
||||
"last_login": "last_login",
|
||||
}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return User.objects.prefetch_related(
|
||||
"groupuser_set", "groupuser_set__group"
|
||||
).all()
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin"],
|
||||
operation_id="admin_list_users",
|
||||
description="Returns all users with detailed information on each user, "
|
||||
"if the requesting user is staff. \n\nThis is a **premium** feature.",
|
||||
**AdminListingView.get_extend_schema_parameters(
|
||||
"users", serializer_class, search_fields, sort_field_mapping
|
||||
),
|
||||
)
|
||||
def get(self, *args, **kwargs):
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class UserAdminView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin"],
|
||||
request=UserAdminUpdateSerializer,
|
||||
operation_id="admin_edit_user",
|
||||
description=f"Updates specified user attributes and returns the updated user if"
|
||||
f" the requesting user is staff. You cannot update yourself to no longer be an "
|
||||
f"admin or active. \n\nThis is a **premium** feature.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="user_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The id of the user to edit",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: UserAdminResponseSerializer(),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
"USER_ADMIN_CANNOT_DEACTIVATE_SELF",
|
||||
"USER_ADMIN_UNKNOWN_USER",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@validate_body(UserAdminUpdateSerializer, partial=True)
|
||||
@map_exceptions(
|
||||
{
|
||||
CannotDeactivateYourselfException: USER_ADMIN_CANNOT_DEACTIVATE_SELF,
|
||||
UserDoesNotExistException: USER_ADMIN_UNKNOWN_USER,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def patch(self, request, user_id, data):
|
||||
"""
|
||||
Updates the specified user with the supplied attributes. Will raise an exception
|
||||
if you attempt un-staff or de-activate yourself.
|
||||
"""
|
||||
|
||||
user_id = int(user_id)
|
||||
|
||||
handler = UserAdminHandler()
|
||||
user = handler.update_user(request.user, user_id, **data)
|
||||
|
||||
return Response(UserAdminResponseSerializer(user).data)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Admin"],
|
||||
operation_id="admin_delete_user",
|
||||
description="Deletes the specified user, if the requesting user has admin "
|
||||
"permissions. You cannot delete yourself. \n\nThis is a **premium** feature.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="user_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The id of the user to delete",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: None,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"USER_ADMIN_CANNOT_DELETE_SELF",
|
||||
"USER_ADMIN_UNKNOWN_USER",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
CannotDeleteYourselfException: USER_ADMIN_CANNOT_DELETE_SELF,
|
||||
UserDoesNotExistException: USER_ADMIN_UNKNOWN_USER,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def delete(self, request, user_id):
|
||||
"""
|
||||
Deletes the specified user. Raises an exception if you attempt to delete
|
||||
yourself.
|
||||
"""
|
||||
|
||||
user_id = int(user_id)
|
||||
|
||||
handler = UserAdminHandler()
|
||||
handler.delete_user(request.user, user_id)
|
||||
|
||||
return Response(status=204)
|
203
premium/backend/src/baserow_premium/api/admin/views.py
Normal file
203
premium/backend/src/baserow_premium/api/admin/views.py
Normal file
|
@ -0,0 +1,203 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import map_exceptions
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow.api.pagination import PageNumberPagination
|
||||
from baserow_premium.api.admin.errors import (
|
||||
ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE,
|
||||
ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION,
|
||||
)
|
||||
from baserow_premium.admin.exceptions import (
|
||||
InvalidSortDirectionException,
|
||||
InvalidSortAttributeException,
|
||||
)
|
||||
|
||||
|
||||
class AdminListingView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
serializer_class = None
|
||||
search_fields = ["id"]
|
||||
sort_field_mapping = {}
|
||||
|
||||
@map_exceptions(
|
||||
{
|
||||
InvalidSortDirectionException: ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION,
|
||||
InvalidSortAttributeException: ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE,
|
||||
}
|
||||
)
|
||||
def get(self, request):
|
||||
"""
|
||||
Responds with paginated results related to queryset and the serializer
|
||||
defined on this class.
|
||||
"""
|
||||
|
||||
search = request.GET.get("search")
|
||||
sorts = request.GET.get("sorts")
|
||||
|
||||
queryset = self.get_queryset(request)
|
||||
queryset = self._apply_sorts_or_default_sort(sorts, queryset)
|
||||
queryset = self._apply_search(search, queryset)
|
||||
|
||||
paginator = PageNumberPagination(limit_page_size=100)
|
||||
page = paginator.paginate_queryset(queryset, request, self)
|
||||
serializer = self.get_serializer(request, page, many=True)
|
||||
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def get_queryset(self, request):
|
||||
raise NotImplementedError("The get_queryset method must be set.")
|
||||
|
||||
def get_serializer(self, request, *args, **kwargs):
|
||||
if not self.serializer_class:
|
||||
raise NotImplementedError(
|
||||
"Either the serializer_class must be set or the get_serializer method "
|
||||
"must be overwritten."
|
||||
)
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def _apply_search(self, search, queryset):
|
||||
"""
|
||||
Applies the provided search query to the provided query. If the search query
|
||||
is provided then an `icontains` lookup will be done for each field in the
|
||||
search_fields property. One of the fields has to match the query.
|
||||
|
||||
:param search: The search query.
|
||||
:type search: str or None
|
||||
:param queryset: The queryset where the search query must be applied to.
|
||||
:type queryset: QuerySet
|
||||
:return: The queryset filtering the results by the search query.
|
||||
:rtype: QuerySet
|
||||
"""
|
||||
|
||||
if not search:
|
||||
return queryset
|
||||
|
||||
q = Q()
|
||||
|
||||
for search_field in self.search_fields:
|
||||
q.add(Q(**{f"{search_field}__icontains": search}), Q.OR)
|
||||
|
||||
return queryset.filter(q)
|
||||
|
||||
def _apply_sorts_or_default_sort(self, sorts: str, queryset):
|
||||
"""
|
||||
Takes a comma separated string in the form of +attribute,-attribute2 and
|
||||
applies them to a django queryset in order.
|
||||
Defaults to sorting by id if no sorts are provided.
|
||||
Raises an InvalidSortDirectionException if an attribute does not begin with `+`
|
||||
or `-`.
|
||||
Raises an InvalidSortAttributeException if an unknown attribute is supplied to
|
||||
sort by or multiple of the same attribute are provided.
|
||||
|
||||
:param sorts: The list of sorts to apply to the queryset.
|
||||
:param queryset: The queryset to sort.
|
||||
:return: The sorted queryset.
|
||||
"""
|
||||
|
||||
if sorts is None:
|
||||
return queryset.order_by("id")
|
||||
|
||||
parsed_django_order_bys = []
|
||||
already_seen_sorts = set()
|
||||
for s in sorts.split(","):
|
||||
if len(s) <= 2:
|
||||
raise InvalidSortAttributeException()
|
||||
|
||||
sort_direction_prefix = s[0]
|
||||
sort_field_name = s[1:]
|
||||
|
||||
try:
|
||||
sort_direction_to_django_prefix = {"+": "", "-": "-"}
|
||||
direction = sort_direction_to_django_prefix[sort_direction_prefix]
|
||||
except KeyError:
|
||||
raise InvalidSortDirectionException()
|
||||
|
||||
try:
|
||||
attribute = self.sort_field_mapping[sort_field_name]
|
||||
except KeyError:
|
||||
raise InvalidSortAttributeException()
|
||||
|
||||
if attribute in already_seen_sorts:
|
||||
raise InvalidSortAttributeException()
|
||||
else:
|
||||
already_seen_sorts.add(attribute)
|
||||
|
||||
parsed_django_order_bys.append(f"{direction}{attribute}")
|
||||
|
||||
return queryset.order_by(*parsed_django_order_bys)
|
||||
|
||||
@staticmethod
|
||||
def get_extend_schema_parameters(
|
||||
name, serializer_class, search_fields, sort_field_mapping
|
||||
):
|
||||
"""
|
||||
Returns the schema properties that can be used in in the @extend_schema
|
||||
decorator.
|
||||
"""
|
||||
|
||||
fields = sort_field_mapping.keys()
|
||||
all_fields = ", ".join(fields)
|
||||
field_name_1 = "field_1"
|
||||
field_name_2 = "field_2"
|
||||
for i, field in enumerate(fields):
|
||||
if i == 0:
|
||||
field_name_1 = field
|
||||
if i == 1:
|
||||
field_name_2 = field
|
||||
|
||||
return {
|
||||
"parameters": [
|
||||
OpenApiParameter(
|
||||
name="search",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description=f"If provided only {name} that match the query will "
|
||||
f"be returned.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="sorts",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description=f"A comma separated string of attributes to sort by, "
|
||||
f"each attribute must be prefixed with `+` for a descending "
|
||||
f"sort or a `-` for an ascending sort. The accepted attribute "
|
||||
f"names are: {all_fields}. For example `sorts=-{field_name_1},"
|
||||
f"-{field_name_2}` will sort the {name} first by descending "
|
||||
f"{field_name_1} and then ascending {field_name_2}. A sort"
|
||||
f"parameter with multiple instances of the same sort attribute "
|
||||
f"will respond with the ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE "
|
||||
f"error.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Defines which page should be returned.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="size",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description=f"Defines how many {name} should be returned per "
|
||||
f"page.",
|
||||
),
|
||||
],
|
||||
"responses": {
|
||||
200: serializer_class(many=True),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_PAGE_SIZE_LIMIT",
|
||||
"ERROR_INVALID_PAGE",
|
||||
"ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION",
|
||||
"ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from .user_admin import urls as user_admin_urls
|
||||
from .admin_dashboard import urls as admin_dashboard_urls
|
||||
|
||||
from .admin import urls as admin_urls
|
||||
|
||||
app_name = "baserow_premium.api"
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/user/", include(user_admin_urls, namespace="admin_user")),
|
||||
path(
|
||||
"admin/dashboard/", include(admin_dashboard_urls, namespace="admin_dashboard")
|
||||
),
|
||||
path("admin/", include(admin_urls, namespace="admin")),
|
||||
]
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from baserow_premium.api.user_admin.views import UsersAdminView, UserAdminView
|
||||
|
||||
|
||||
app_name = "baserow_premium.api.user_admin"
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", UsersAdminView.as_view(), name="users"),
|
||||
url(r"^(?P<user_id>[0-9]+)/$", UserAdminView.as_view(), name="user_edit"),
|
||||
]
|
|
@ -1,209 +0,0 @@
|
|||
from django.db import transaction
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import validate_body, map_exceptions
|
||||
from baserow.api.pagination import PageNumberPagination
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow_premium.api.user_admin.errors import (
|
||||
USER_ADMIN_INVALID_SORT_DIRECTION,
|
||||
USER_ADMIN_INVALID_SORT_ATTRIBUTE,
|
||||
USER_ADMIN_CANNOT_DEACTIVATE_SELF,
|
||||
USER_ADMIN_CANNOT_DELETE_SELF,
|
||||
USER_ADMIN_UNKNOWN_USER,
|
||||
)
|
||||
from baserow_premium.api.user_admin.serializers import (
|
||||
UserAdminUpdateSerializer,
|
||||
UserAdminResponseSerializer,
|
||||
)
|
||||
from baserow_premium.user_admin.exceptions import (
|
||||
CannotDeactivateYourselfException,
|
||||
CannotDeleteYourselfException,
|
||||
UserDoesNotExistException,
|
||||
InvalidSortDirectionException,
|
||||
InvalidSortAttributeException,
|
||||
)
|
||||
from baserow_premium.user_admin.handler import (
|
||||
UserAdminHandler,
|
||||
allowed_user_admin_sort_field_names,
|
||||
)
|
||||
|
||||
|
||||
class UsersAdminView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
_valid_sortable_fields = ",".join(allowed_user_admin_sort_field_names())
|
||||
|
||||
@extend_schema(
|
||||
tags=["Users"],
|
||||
operation_id="list_users",
|
||||
description="Returns all baserow users with detailed information on each user, "
|
||||
"if the requesting user has admin permissions.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="search",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="If provided only users with a username that matches the "
|
||||
"search query will be returned.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="sorts",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="A comma separated string of user attributes to sort by, "
|
||||
"each attribute must be prefixed with `+` for a descending "
|
||||
"sort or a `-` for an ascending sort. The accepted attribute names "
|
||||
f"are: {_valid_sortable_fields}. "
|
||||
"For example `sorts=-username,+is_active` will sort the "
|
||||
"results first by descending username and then ascending is_active."
|
||||
"A sort parameter with multiple instances of the same "
|
||||
"sort attribute will respond with the USER_ADMIN_INVALID_SORT_ATTRIBUTE"
|
||||
"error.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Defines which page of users should be returned.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="size",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Defines how many users should be returned per page.",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: UserAdminResponseSerializer(many=True),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_PAGE_SIZE_LIMIT",
|
||||
"ERROR_INVALID_PAGE",
|
||||
"USER_ADMIN_INVALID_SORT_DIRECTION",
|
||||
"USER_ADMIN_INVALID_SORT_ATTRIBUTE",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
InvalidSortDirectionException: USER_ADMIN_INVALID_SORT_DIRECTION,
|
||||
InvalidSortAttributeException: USER_ADMIN_INVALID_SORT_ATTRIBUTE,
|
||||
}
|
||||
)
|
||||
def get(self, request):
|
||||
"""
|
||||
Lists all the users of a user, optionally filtering on username by the
|
||||
'search' get parameter, optionally sorting by the 'sorts' get parameter.
|
||||
"""
|
||||
|
||||
search = request.GET.get("search")
|
||||
sorts = request.GET.get("sorts")
|
||||
|
||||
handler = UserAdminHandler()
|
||||
users = handler.get_users(request.user, search, sorts)
|
||||
|
||||
paginator = PageNumberPagination(limit_page_size=100)
|
||||
page = paginator.paginate_queryset(users, request, self)
|
||||
serializer = UserAdminResponseSerializer(page, many=True)
|
||||
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
|
||||
class UserAdminView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Users"],
|
||||
request=UserAdminUpdateSerializer,
|
||||
operation_id="edit_user",
|
||||
description=f"Updates specified user attributes and returns the updated user if"
|
||||
f" the requesting user has admin permissions. You cannot update yourself to no "
|
||||
f"longer be an admin or active.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="user_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The id of the user to edit",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: UserAdminResponseSerializer(),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
"USER_ADMIN_CANNOT_DEACTIVATE_SELF",
|
||||
"USER_ADMIN_UNKNOWN_USER",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@validate_body(UserAdminUpdateSerializer, partial=True)
|
||||
@map_exceptions(
|
||||
{
|
||||
CannotDeactivateYourselfException: USER_ADMIN_CANNOT_DEACTIVATE_SELF,
|
||||
UserDoesNotExistException: USER_ADMIN_UNKNOWN_USER,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def patch(self, request, user_id, data):
|
||||
"""
|
||||
Updates the specified user with the supplied attributes. Will raise an exception
|
||||
if you attempt un-staff or de-activate yourself.
|
||||
"""
|
||||
|
||||
user_id = int(user_id)
|
||||
|
||||
handler = UserAdminHandler()
|
||||
user = handler.update_user(request.user, user_id, **data)
|
||||
|
||||
return Response(UserAdminResponseSerializer(user).data)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Users"],
|
||||
operation_id="delete_user",
|
||||
description="Deletes the specified user, if the requesting user has admin "
|
||||
"permissions. You cannot delete yourself.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="user_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The id of the user to delete",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: None,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"USER_ADMIN_CANNOT_DELETE_SELF",
|
||||
"USER_ADMIN_UNKNOWN_USER",
|
||||
]
|
||||
),
|
||||
401: None,
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
CannotDeleteYourselfException: USER_ADMIN_CANNOT_DELETE_SELF,
|
||||
UserDoesNotExistException: USER_ADMIN_UNKNOWN_USER,
|
||||
}
|
||||
)
|
||||
def delete(self, request, user_id):
|
||||
"""
|
||||
Deletes the specified user. Raises an exception if you attempt to delete
|
||||
yourself.
|
||||
"""
|
||||
|
||||
user_id = int(user_id)
|
||||
|
||||
handler = UserAdminHandler()
|
||||
handler.delete_user(request.user, user_id)
|
||||
|
||||
return Response()
|
|
@ -1,213 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from baserow.core.exceptions import IsNotAdminError
|
||||
from baserow_premium.user_admin.exceptions import (
|
||||
CannotDeactivateYourselfException,
|
||||
CannotDeleteYourselfException,
|
||||
UserDoesNotExistException,
|
||||
InvalidSortDirectionException,
|
||||
InvalidSortAttributeException,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def sort_field_names_to_user_attributes():
|
||||
return {
|
||||
"id": "id",
|
||||
"is_active": "is_active",
|
||||
"name": "first_name",
|
||||
"username": "username",
|
||||
"date_joined": "date_joined",
|
||||
"last_login": "last_login",
|
||||
}
|
||||
|
||||
|
||||
def allowed_user_admin_sort_field_names():
|
||||
return sort_field_names_to_user_attributes().keys()
|
||||
|
||||
|
||||
class UserAdminHandler:
|
||||
def get_users(
|
||||
self,
|
||||
requesting_user: User,
|
||||
username_search: Optional[str] = None,
|
||||
sorts: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Looks up all users, performs an optional username search and then sorts the
|
||||
resulting user queryset and returns it. By default if no sorts are provided
|
||||
sorts by user id ascending.
|
||||
|
||||
:param requesting_user: The user who is making the request to get_users, the
|
||||
user must be a staff member or else an exception will
|
||||
be raised.
|
||||
:param username_search: An optional icontains username search to filter the
|
||||
returned users by.
|
||||
:param sorts: A comma separated string like `+username,-id` to be applied as
|
||||
an ordering order over the returned users. Prefix the attribute with +
|
||||
for an ascending sort, - for descending.
|
||||
See `allowed_user_admin_sort_field_names` for the allowed attributes to
|
||||
sort by.
|
||||
Raises InvalidSortAttributeException or InvalidSortAttributeException if
|
||||
an invalid sort string is provided.
|
||||
:return: A queryset of users in Baserow, optionally sorted and ordered by the
|
||||
specified parameters.
|
||||
"""
|
||||
|
||||
self._raise_if_not_permitted(requesting_user)
|
||||
|
||||
users = User.objects.prefetch_related(
|
||||
"groupuser_set", "groupuser_set__group"
|
||||
).all()
|
||||
if username_search is not None:
|
||||
users = users.filter(username__icontains=username_search)
|
||||
|
||||
users = self._apply_sorts_or_default_sort(sorts, users)
|
||||
|
||||
return users
|
||||
|
||||
@staticmethod
|
||||
def _apply_sorts_or_default_sort(sorts: str, queryset):
|
||||
"""
|
||||
Takes a comma separated string in the form of +attribute,-attribute2 and
|
||||
applies them to a django queryset in order.
|
||||
Defaults to sorting by id if no sorts are provided.
|
||||
Raises an InvalidSortDirectionException if an attribute does not begin with `+`
|
||||
or `-`.
|
||||
Raises an InvalidSortAttributeException if an unknown attribute is supplied to
|
||||
sort by or multiple of the same attribute are provided.
|
||||
|
||||
:param sorts: The list of sorts to apply to the queryset.
|
||||
:param queryset: The queryset to sort.
|
||||
:return: The sorted queryset.
|
||||
"""
|
||||
|
||||
if sorts is None:
|
||||
return queryset.order_by("id")
|
||||
|
||||
parsed_django_order_bys = []
|
||||
already_seen_sorts = set()
|
||||
for s in sorts.split(","):
|
||||
if len(s) <= 2:
|
||||
raise InvalidSortAttributeException()
|
||||
|
||||
sort_direction_prefix = s[0]
|
||||
sort_field_name = s[1:]
|
||||
|
||||
try:
|
||||
sort_direction_to_django_prefix = {"+": "", "-": "-"}
|
||||
direction = sort_direction_to_django_prefix[sort_direction_prefix]
|
||||
except KeyError:
|
||||
raise InvalidSortDirectionException()
|
||||
|
||||
try:
|
||||
attribute = sort_field_names_to_user_attributes()[sort_field_name]
|
||||
except KeyError:
|
||||
raise InvalidSortAttributeException()
|
||||
|
||||
if attribute in already_seen_sorts:
|
||||
raise InvalidSortAttributeException()
|
||||
else:
|
||||
already_seen_sorts.add(attribute)
|
||||
|
||||
parsed_django_order_bys.append(f"{direction}{attribute}")
|
||||
|
||||
return queryset.order_by(*parsed_django_order_bys)
|
||||
|
||||
def update_user(
|
||||
self,
|
||||
requesting_user: User,
|
||||
user_id: int,
|
||||
username: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_staff: Optional[bool] = None,
|
||||
):
|
||||
"""
|
||||
Updates a specified user with new attribute values. Will raise an exception
|
||||
if a user attempts to de-activate or un-staff themselves.
|
||||
|
||||
:param requesting_user: The user who is making the request to update a user, the
|
||||
user must be a staff member or else an exception will
|
||||
be raised.
|
||||
:param user_id: The id of the user to update, if they do not exist raises a
|
||||
UserDoesNotExistException.
|
||||
:param is_staff: Optional value used to set if the user is an admin or not.
|
||||
:param is_active: Optional value to disable or enable login for the user.
|
||||
:param password: Optional new password to securely set for the user.
|
||||
:param name: Optional new name to set on the user.
|
||||
:param username: Optional new username/email to set for the user.
|
||||
"""
|
||||
|
||||
self._raise_if_not_permitted(requesting_user)
|
||||
self._raise_if_locking_self_out_of_admin(
|
||||
is_active, is_staff, requesting_user, user_id
|
||||
)
|
||||
|
||||
try:
|
||||
user = User.objects.select_for_update().get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise UserDoesNotExistException()
|
||||
|
||||
if is_staff is not None:
|
||||
user.is_staff = is_staff
|
||||
if is_active is not None:
|
||||
user.is_active = is_active
|
||||
if password is not None:
|
||||
user.set_password(password)
|
||||
if name is not None:
|
||||
user.first_name = name
|
||||
if username is not None:
|
||||
user.email = username
|
||||
user.username = username
|
||||
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _raise_if_locking_self_out_of_admin(
|
||||
is_active, is_staff, requesting_user, user_id
|
||||
):
|
||||
"""
|
||||
Raises an exception if the requesting_user is about to lock themselves out of
|
||||
the admin area of Baserow by either turning off their staff status or disabling
|
||||
their account.
|
||||
"""
|
||||
|
||||
is_setting_staff_to_false = is_staff is not None and not is_staff
|
||||
is_setting_active_to_false = is_active is not None and not is_active
|
||||
if user_id == requesting_user.id and (
|
||||
is_setting_staff_to_false or is_setting_active_to_false
|
||||
):
|
||||
raise CannotDeactivateYourselfException()
|
||||
|
||||
def delete_user(self, requesting_user: User, user_id: int):
|
||||
"""
|
||||
Deletes a specified user, raises an exception if you attempt to delete yourself.
|
||||
|
||||
:param requesting_user: The user who is making the delete request , the
|
||||
user must be a staff member or else an exception will
|
||||
be raised.
|
||||
:param user_id: The id of the user to update, if they do not exist raises a
|
||||
UnknownUserException.
|
||||
"""
|
||||
|
||||
self._raise_if_not_permitted(requesting_user)
|
||||
|
||||
if requesting_user.id == user_id:
|
||||
raise CannotDeleteYourselfException()
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
user.delete()
|
||||
except User.DoesNotExist:
|
||||
raise UserDoesNotExistException()
|
||||
|
||||
@staticmethod
|
||||
def _raise_if_not_permitted(requesting_user):
|
||||
if not requesting_user.is_staff:
|
||||
raise IsNotAdminError()
|
|
@ -5,7 +5,7 @@ from datetime import timedelta, datetime, date
|
|||
|
||||
from baserow.core.models import UserLogEntry
|
||||
|
||||
from baserow_premium.admin_dashboard.handler import AdminDashboardHandler
|
||||
from baserow_premium.admin.dashboard.handler import AdminDashboardHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
|
@ -0,0 +1,42 @@
|
|||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.db import connection
|
||||
|
||||
from baserow.contrib.database.models import Database, Table
|
||||
from baserow.core.exceptions import IsNotAdminError
|
||||
from baserow.core.models import (
|
||||
Group,
|
||||
GroupUser,
|
||||
)
|
||||
from baserow_premium.admin.groups.handler import GroupsAdminHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.core.signals.group_deleted.send")
|
||||
def test_delete_group(send_mock, data_fixture):
|
||||
staff_user = data_fixture.create_user(is_staff=True)
|
||||
normal_user = data_fixture.create_user(is_staff=False)
|
||||
other_user = data_fixture.create_user()
|
||||
group_1 = data_fixture.create_group(user=other_user)
|
||||
database = data_fixture.create_database_application(group=group_1)
|
||||
table = data_fixture.create_database_table(database=database)
|
||||
|
||||
handler = GroupsAdminHandler()
|
||||
|
||||
with pytest.raises(IsNotAdminError):
|
||||
handler.delete_group(normal_user, group_1)
|
||||
|
||||
handler.delete_group(staff_user, group_1)
|
||||
|
||||
send_mock.assert_called_once()
|
||||
assert send_mock.call_args[1]["group"].id == group_1.id
|
||||
assert "user" not in send_mock.call_args[1]
|
||||
assert len(send_mock.call_args[1]["group_users"]) == 1
|
||||
assert send_mock.call_args[1]["group_users"][0].id == other_user.id
|
||||
|
||||
assert Database.objects.all().count() == 0
|
||||
assert Table.objects.all().count() == 0
|
||||
assert f"database_table_{table.id}" not in connection.introspection.table_names()
|
||||
assert Group.objects.all().count() == 0
|
||||
assert GroupUser.objects.all().count() == 0
|
|
@ -1,104 +1,19 @@
|
|||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.utils.datetime_safe import datetime
|
||||
|
||||
from baserow.core.exceptions import IsNotAdminError
|
||||
from baserow_premium.user_admin.exceptions import (
|
||||
from baserow_premium.admin.users.exceptions import (
|
||||
CannotDeactivateYourselfException,
|
||||
CannotDeleteYourselfException,
|
||||
UserDoesNotExistException,
|
||||
InvalidSortDirectionException,
|
||||
InvalidSortAttributeException,
|
||||
)
|
||||
from baserow_premium.user_admin.handler import (
|
||||
from baserow_premium.admin.users.handler import (
|
||||
UserAdminHandler,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_can_get_users(data_fixture):
|
||||
handler = UserAdminHandler()
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
assert handler.get_users(admin_user).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_non_admin_cant_get_users(data_fixture):
|
||||
handler = UserAdminHandler()
|
||||
non_admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=False,
|
||||
)
|
||||
with pytest.raises(IsNotAdminError):
|
||||
handler.get_users(non_admin_user)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_can_search_by_username(data_fixture):
|
||||
handler = UserAdminHandler()
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
data_fixture.create_user(
|
||||
email="other_user@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
results = handler.get_users(admin_user, username_search="other_user")
|
||||
assert results.count() == 1
|
||||
assert results[0].username == "other_user@test.nl"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_can_sort_by_multiple_fields_in_specified_order_and_directions(
|
||||
data_fixture,
|
||||
):
|
||||
handler = UserAdminHandler()
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
is_active=False,
|
||||
date_joined=datetime(2020, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
data_fixture.create_user(
|
||||
email="other_user1@test.nl",
|
||||
password="password",
|
||||
first_name="Test2",
|
||||
is_staff=True,
|
||||
is_active=True,
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
data_fixture.create_user(
|
||||
email="other_user2@test.nl",
|
||||
password="password",
|
||||
first_name="Test3",
|
||||
is_staff=True,
|
||||
is_active=True,
|
||||
date_joined=datetime(2022, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
results = handler.get_users(admin_user, sorts="-is_active,+date_joined")
|
||||
assert results.count() == 3
|
||||
assert results[0].first_name == "Test2"
|
||||
assert results[1].first_name == "Test3"
|
||||
assert results[2].first_name == "Test1"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_can_delete_user(data_fixture):
|
||||
handler = UserAdminHandler()
|
||||
|
@ -334,83 +249,3 @@ def test_raises_exception_when_updating_an_unknown_user(data_fixture):
|
|||
)
|
||||
with pytest.raises(UserDoesNotExistException):
|
||||
handler.update_user(admin_user, 99999, username="new_password")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_sort_direction_not_provided(api_client, data_fixture):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortDirectionException):
|
||||
handler.get_users(admin_user, sorts="username")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_invalid_sort_direction_provided(api_client, data_fixture):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortDirectionException):
|
||||
handler.get_users(admin_user, sorts="*username")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_invalid_sorts_mixed_with_valid_ones(api_client, data_fixture):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortAttributeException):
|
||||
handler.get_users(admin_user, sorts="+username,-idd")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_multiple_of_the_same_sort_attr_are_given(
|
||||
api_client, data_fixture
|
||||
):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortAttributeException):
|
||||
handler.get_users(admin_user, sorts="+username,-id,-username")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_blank_sorts_provided(api_client, data_fixture):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortAttributeException):
|
||||
handler.get_users(admin_user, sorts=",,")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_throws_error_if_empty_sorts_provided(api_client, data_fixture):
|
||||
admin_user = data_fixture.create_user(
|
||||
email="test@test.nl",
|
||||
password="password",
|
||||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
handler = UserAdminHandler()
|
||||
with pytest.raises(InvalidSortAttributeException):
|
||||
handler.get_users(admin_user, sorts="")
|
|
@ -25,14 +25,14 @@ def test_admin_dashboard(api_client, data_fixture):
|
|||
UserLogEntry.objects.create(actor=admin_user, action="SIGNED_IN")
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_dashboard:dashboard"),
|
||||
reverse("api:premium:admin:dashboard:dashboard"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {normal_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_dashboard:dashboard"),
|
||||
reverse("api:premium:admin:dashboard:dashboard"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {admin_token}",
|
||||
)
|
||||
|
@ -57,7 +57,7 @@ def test_admin_dashboard(api_client, data_fixture):
|
|||
"active_users_per_day": [{"date": "2020-01-01", "count": 1}],
|
||||
}
|
||||
|
||||
url = reverse("api:premium:admin_dashboard:dashboard")
|
||||
url = reverse("api:premium:admin:dashboard:dashboard")
|
||||
response = api_client.get(
|
||||
f"{url}?timezone=Etc/GMT%2B1",
|
||||
format="json",
|
|
@ -0,0 +1,177 @@
|
|||
import pytest
|
||||
|
||||
from django.utils.timezone import make_aware, datetime, utc
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_403_FORBIDDEN,
|
||||
HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
from baserow.core.models import Group
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_admin_groups(api_client, data_fixture, django_assert_num_queries):
|
||||
"""
|
||||
This endpoint doesn't need to be tested extensively because it uses the same base
|
||||
class as the list users endpoint which already has extensive tests. We only need to
|
||||
test if the functionality works in a basic form.
|
||||
"""
|
||||
|
||||
created = make_aware(datetime(2020, 4, 10, 0, 0, 0), utc)
|
||||
staff_user, staff_token = data_fixture.create_user_and_token(is_staff=True)
|
||||
normal_user, normal_token = data_fixture.create_user_and_token()
|
||||
group_1 = data_fixture.create_group(name="A")
|
||||
group_1.created_on = created
|
||||
group_1.save()
|
||||
group_2 = data_fixture.create_group(name="B", created_on=created)
|
||||
group_2.created_on = created
|
||||
group_2.save()
|
||||
data_fixture.create_user_group(
|
||||
group=group_1, user=normal_user, permissions="MEMBER"
|
||||
)
|
||||
data_fixture.create_user_group(group=group_2, user=normal_user, permissions="ADMIN")
|
||||
data_fixture.create_database_application(group=group_1)
|
||||
data_fixture.create_database_application(group=group_1)
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin:groups:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {normal_token}",
|
||||
)
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
with django_assert_num_queries(5):
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin:groups:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {staff_token}",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json == {
|
||||
"count": 2,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
"id": group_1.id,
|
||||
"name": group_1.name,
|
||||
"users": [
|
||||
{
|
||||
"id": normal_user.id,
|
||||
"email": normal_user.email,
|
||||
"permissions": "MEMBER",
|
||||
}
|
||||
],
|
||||
"application_count": 2,
|
||||
"created_on": "2020-04-10T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": group_2.id,
|
||||
"name": group_2.name,
|
||||
"users": [
|
||||
{
|
||||
"id": normal_user.id,
|
||||
"email": normal_user.email,
|
||||
"permissions": "ADMIN",
|
||||
}
|
||||
],
|
||||
"application_count": 0,
|
||||
"created_on": "2020-04-10T00:00:00Z",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
response = api_client.get(
|
||||
f'{reverse("api:premium:admin:groups:list")}?search={group_1.name}',
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {staff_token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json == {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
"id": group_1.id,
|
||||
"name": group_1.name,
|
||||
"users": [
|
||||
{
|
||||
"id": normal_user.id,
|
||||
"email": normal_user.email,
|
||||
"permissions": "MEMBER",
|
||||
}
|
||||
],
|
||||
"application_count": 2,
|
||||
"created_on": "2020-04-10T00:00:00Z",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
response = api_client.get(
|
||||
f'{reverse("api:premium:admin:groups:list")}?sorts=-name',
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {staff_token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json == {
|
||||
"count": 2,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
"id": group_2.id,
|
||||
"name": group_2.name,
|
||||
"users": [
|
||||
{
|
||||
"id": normal_user.id,
|
||||
"email": normal_user.email,
|
||||
"permissions": "ADMIN",
|
||||
}
|
||||
],
|
||||
"application_count": 0,
|
||||
"created_on": "2020-04-10T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": group_1.id,
|
||||
"name": group_1.name,
|
||||
"users": [
|
||||
{
|
||||
"id": normal_user.id,
|
||||
"email": normal_user.email,
|
||||
"permissions": "MEMBER",
|
||||
}
|
||||
],
|
||||
"application_count": 2,
|
||||
"created_on": "2020-04-10T00:00:00Z",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_group(api_client, data_fixture):
|
||||
normal_user, normal_token = data_fixture.create_user_and_token()
|
||||
staff_user, staff_token = data_fixture.create_user_and_token(is_staff=True)
|
||||
group = data_fixture.create_group()
|
||||
|
||||
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": 99999})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {staff_token}")
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_GROUP_DOES_NOT_EXIST"
|
||||
|
||||
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": group.id})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {normal_token}")
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
url = reverse("api:premium:admin:groups:edit", kwargs={"group_id": group.id})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {staff_token}")
|
||||
assert response.status_code == 204
|
||||
assert Group.objects.all().count() == 0
|
|
@ -6,6 +6,7 @@ from django.utils import timezone
|
|||
from django.utils.datetime_safe import datetime
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_403_FORBIDDEN,
|
||||
|
@ -23,7 +24,7 @@ def test_non_admin_cannot_see_admin_users_endpoint(api_client, data_fixture):
|
|||
email="test@test.nl", password="password", first_name="Test1", is_staff=False
|
||||
)
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -52,7 +53,7 @@ def test_admin_can_see_admin_users_endpoint(api_client, data_fixture):
|
|||
permissions=GROUP_USER_PERMISSION_MEMBER,
|
||||
)
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -96,7 +97,7 @@ def test_admin_with_invalid_token_cannot_see_admin_users(api_client, data_fixtur
|
|||
is_staff=True,
|
||||
)
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT abc123",
|
||||
)
|
||||
|
@ -114,7 +115,7 @@ def test_admin_accessing_invalid_user_admin_page_returns_error(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=2",
|
||||
format="json",
|
||||
|
@ -134,7 +135,7 @@ def test_admin_accessing_user_admin_with_invalid_page_size_returns_error(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&size=201",
|
||||
format="json",
|
||||
|
@ -158,7 +159,7 @@ def test_admin_can_search_users(api_client, data_fixture):
|
|||
first_name="Test1",
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&search=specific_user",
|
||||
format="json",
|
||||
|
@ -198,7 +199,7 @@ def test_admin_can_sort_users(api_client, data_fixture):
|
|||
first_name="Test1",
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&search=specific_user",
|
||||
format="json",
|
||||
|
@ -234,14 +235,14 @@ def test_returns_error_response_if_invalid_sort_field_provided(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=-invalid_field_name",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_ATTRIBUTE"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -254,14 +255,14 @@ def test_returns_error_response_if_sort_direction_not_provided(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=username",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_DIRECTION"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -274,14 +275,14 @@ def test_returns_error_response_if_invalid_sort_direction_provided(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=*username",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_DIRECTION"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -294,14 +295,14 @@ def test_returns_error_response_if_invalid_sorts_mixed_with_valid_ones(
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=+username,username,-invalid_field",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_DIRECTION"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_DIRECTION"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -312,14 +313,14 @@ def test_returns_error_response_if_blank_sorts_provided(api_client, data_fixture
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=,,",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_ATTRIBUTE"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -330,14 +331,14 @@ def test_returns_error_response_if_no_sorts_provided(api_client, data_fixture):
|
|||
first_name="Test1",
|
||||
is_staff=True,
|
||||
)
|
||||
url = reverse("api:premium:admin_user:users")
|
||||
url = reverse("api:premium:admin:users:list")
|
||||
response = api_client.get(
|
||||
f"{url}?page=1&sorts=",
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "USER_ADMIN_INVALID_SORT_ATTRIBUTE"
|
||||
assert response.json()["error"] == "ERROR_ADMIN_LISTING_INVALID_SORT_ATTRIBUTE"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -353,9 +354,7 @@ def test_non_admin_cannot_delete_user(api_client, data_fixture):
|
|||
password="password",
|
||||
first_name="Test1",
|
||||
)
|
||||
url = reverse(
|
||||
"api:premium:admin_user:user_edit", kwargs={"user_id": user_to_delete.id}
|
||||
)
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user_to_delete.id})
|
||||
response = api_client.delete(
|
||||
url,
|
||||
format="json",
|
||||
|
@ -377,18 +376,16 @@ def test_admin_can_delete_user(api_client, data_fixture):
|
|||
password="password",
|
||||
first_name="Test1",
|
||||
)
|
||||
url = reverse(
|
||||
"api:premium:admin_user:user_edit", kwargs={"user_id": user_to_delete.id}
|
||||
)
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user_to_delete.id})
|
||||
response = api_client.delete(
|
||||
url,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -404,9 +401,7 @@ def test_non_admin_cannot_patch_user(api_client, data_fixture):
|
|||
first_name="Test1",
|
||||
is_staff=False,
|
||||
)
|
||||
url = reverse(
|
||||
"api:premium:admin_user:user_edit", kwargs={"user_id": non_admin_user.id}
|
||||
)
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": non_admin_user.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{"username": "some_other_email@test.nl"},
|
||||
|
@ -428,7 +423,7 @@ def test_admin_can_patch_user(api_client, data_fixture):
|
|||
is_staff=True,
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:user_edit", kwargs={"user_id": user.id})
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
|
||||
old_password = user.password
|
||||
response = api_client.patch(
|
||||
url,
|
||||
|
@ -460,7 +455,7 @@ def test_error_returned_when_invalid_field_supplied_to_edit(api_client, data_fix
|
|||
is_staff=True,
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:user_edit", kwargs={"user_id": user.id})
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
|
||||
|
||||
# We have to provide a str as otherwise the test api client will "helpfully" try
|
||||
# to serialize the dict using the endpoints serializer, which will fail before
|
||||
|
@ -484,7 +479,7 @@ def test_error_returned_when_updating_user_with_invalid_email(api_client, data_f
|
|||
is_staff=True,
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:user_edit", kwargs={"user_id": user.id})
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
|
||||
|
||||
# We have to provide a str as otherwise the test api client will "helpfully" try
|
||||
# to serialize the dict using the endpoints serializer, which will fail before
|
||||
|
@ -510,7 +505,7 @@ def test_error_returned_when_valid_and_invalid_fields_supplied_to_edit(
|
|||
is_staff=True,
|
||||
date_joined=datetime(2021, 4, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
url = reverse("api:premium:admin_user:user_edit", kwargs={"user_id": user.id})
|
||||
url = reverse("api:premium:admin:users:edit", kwargs={"user_id": user.id})
|
||||
|
||||
# We have to provide a str as otherwise the test api client will "helpfully" try
|
||||
# to serialize the dict using the endpoints serializer, which will fail before
|
||||
|
@ -542,7 +537,7 @@ def test_admin_getting_view_users_only_runs_two_queries_instead_of_n(
|
|||
|
||||
with django_assert_num_queries(fixed_num_of_queries_unrelated_to_number_of_rows):
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -555,7 +550,7 @@ def test_admin_getting_view_users_only_runs_two_queries_instead_of_n(
|
|||
|
||||
with django_assert_num_queries(fixed_num_of_queries_unrelated_to_number_of_rows):
|
||||
response = api_client.get(
|
||||
reverse("api:premium:admin_user:users"),
|
||||
reverse("api:premium:admin:users:list"),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
|
@ -18,7 +18,7 @@ export class DashboardType extends AdminType {
|
|||
}
|
||||
|
||||
getOrder() {
|
||||
return 0
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,28 @@ export class UsersAdminType extends AdminType {
|
|||
}
|
||||
|
||||
getOrder() {
|
||||
return 1
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupsAdminType extends AdminType {
|
||||
static getType() {
|
||||
return 'groups'
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return 'layer-group'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Groups'
|
||||
}
|
||||
|
||||
getRouteName() {
|
||||
return 'admin-groups'
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import 'user_admin';
|
||||
@import 'crud_table';
|
||||
@import 'admin_dashboard';
|
||||
@import 'user_admin';
|
||||
@import 'group_admin';
|
||||
@import 'crud_table';
|
||||
|
|
|
@ -150,7 +150,6 @@
|
|||
content: '';
|
||||
z-index: 4;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
|
||||
@include absolute(0, 0, 0, 0);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.group-admin-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.group-admin-name__name {
|
||||
@extend %ellipsis;
|
||||
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.group-admin-name__menu {
|
||||
@include fixed-height(24px, 14px);
|
||||
|
||||
border-radius: 3px;
|
||||
color: $color-primary-900;
|
||||
padding: 0 5px;
|
||||
margin-left: auto;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: $color-neutral-100;
|
||||
}
|
||||
}
|
|
@ -121,6 +121,7 @@
|
|||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 9px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<CrudTable
|
||||
:left-columns="leftColumns"
|
||||
:right-columns="rightColumns"
|
||||
:service="service"
|
||||
row-id-key="id"
|
||||
@edit-group="displayEditGroupContext"
|
||||
@show-hidden-groups="displayHiddenUsers"
|
||||
@row-context="onRowContext"
|
||||
>
|
||||
<template #header>
|
||||
<div class="crudtable__header-title">All groups</div>
|
||||
</template>
|
||||
<template #menus="slotProps">
|
||||
<EditGroupContext
|
||||
ref="editGroupContext"
|
||||
:group="editGroup"
|
||||
@group-deleted="slotProps.deleteRow"
|
||||
>
|
||||
</EditGroupContext>
|
||||
<HiddenUsersContext
|
||||
ref="hiddenUsersContext"
|
||||
:hidden-values="hiddenUsers"
|
||||
></HiddenUsersContext>
|
||||
</template>
|
||||
</CrudTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupsAdminService from '@baserow_premium/services/admin/groups'
|
||||
import CrudTable from '@baserow_premium/components/crud_table/CrudTable'
|
||||
import SimpleField from '@baserow_premium/components/crud_table/fields/SimpleField'
|
||||
import LocalDateField from '@baserow_premium/components/crud_table/fields/LocalDateField'
|
||||
import GroupUsersField from '@baserow_premium/components/admin/groups/fields/GroupUsersField'
|
||||
import GroupNameField from '@baserow_premium/components/admin/groups/fields/GroupNameField'
|
||||
import EditGroupContext from '@baserow_premium/components/admin/groups/contexts/EditGroupContext'
|
||||
import HiddenUsersContext from '@baserow_premium/components/admin/groups/contexts/HiddenUsersContext'
|
||||
import CrudTableColumn from '@baserow_premium/crud_table/crudTableColumn'
|
||||
|
||||
export default {
|
||||
name: 'GroupsAdminTable',
|
||||
components: {
|
||||
CrudTable,
|
||||
HiddenUsersContext,
|
||||
EditGroupContext,
|
||||
},
|
||||
data() {
|
||||
this.leftColumns = [
|
||||
new CrudTableColumn(
|
||||
'id',
|
||||
'ID',
|
||||
SimpleField,
|
||||
'min-content',
|
||||
'max-content',
|
||||
true
|
||||
),
|
||||
new CrudTableColumn(
|
||||
'name',
|
||||
'Name',
|
||||
GroupNameField,
|
||||
'200px',
|
||||
'max-content',
|
||||
true
|
||||
),
|
||||
]
|
||||
this.rightColumns = [
|
||||
new CrudTableColumn(
|
||||
'users',
|
||||
'Members',
|
||||
GroupUsersField,
|
||||
'100px',
|
||||
'500px'
|
||||
),
|
||||
new CrudTableColumn(
|
||||
'application_count',
|
||||
'Applications',
|
||||
SimpleField,
|
||||
'min-content',
|
||||
'max-content',
|
||||
true
|
||||
),
|
||||
new CrudTableColumn(
|
||||
'created_on',
|
||||
'Created',
|
||||
LocalDateField,
|
||||
'min-content',
|
||||
'200px',
|
||||
true
|
||||
),
|
||||
]
|
||||
this.service = GroupsAdminService(this.$client)
|
||||
return {
|
||||
editGroup: {},
|
||||
hiddenUsers: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
displayEditGroupContext(event) {
|
||||
const action = event.group.id === this.editGroup.id ? 'toggle' : 'show'
|
||||
this.editGroup = event.group
|
||||
this.$refs.editGroupContext[action](event.target, 'bottom', 'left', 4)
|
||||
},
|
||||
onRowContext({ row, event }) {
|
||||
this.displayEditGroupContext({
|
||||
group: row,
|
||||
target: {
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
},
|
||||
})
|
||||
},
|
||||
displayHiddenUsers(event) {
|
||||
const action = this.hiddenUsers === event.hiddenValues ? 'toggle' : 'show'
|
||||
this.hiddenUsers = event.hiddenValues
|
||||
this.$refs.hiddenUsersContext[action](event.target, 'bottom', 'left', 4)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<Context>
|
||||
<template v-if="Object.keys(group).length > 0">
|
||||
<ul class="context__menu">
|
||||
<li>
|
||||
<a @click.prevent="showDeleteModal">
|
||||
<i class="context__menu-icon fas fa-fw fa-trash-alt"></i>
|
||||
Permanently delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<DeleteGroupModal
|
||||
ref="deleteGroupModal"
|
||||
:group="group"
|
||||
@group-deleted="$emit('group-deleted', $event)"
|
||||
></DeleteGroupModal>
|
||||
</template>
|
||||
</Context>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from '@baserow/modules/core/mixins/context'
|
||||
import DeleteGroupModal from '@baserow_premium/components/admin/groups/modals/DeleteGroupModal'
|
||||
|
||||
export default {
|
||||
name: 'EditUserContext',
|
||||
components: { DeleteGroupModal },
|
||||
mixins: [context],
|
||||
props: {
|
||||
group: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDeleteModal() {
|
||||
this.hide()
|
||||
this.$refs.deleteGroupModal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import HiddenGroupsContext from '@baserow_premium/components/admin/users/contexts/HiddenGroupsContext'
|
||||
|
||||
export default {
|
||||
name: 'HiddenUsersContext',
|
||||
extends: HiddenGroupsContext,
|
||||
data() {
|
||||
return {
|
||||
nameKey: 'email',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
<template functional>
|
||||
<div class="group-admin-name" :class="[data.staticClass, data.class]">
|
||||
<div class="group-admin-name__name" :title="props.row[props.column.key]">
|
||||
{{ props.row[props.column.key] }}
|
||||
</div>
|
||||
<a
|
||||
class="group-admin-name__menu"
|
||||
@click.prevent="
|
||||
listeners['edit-group'] &&
|
||||
listeners['edit-group']({
|
||||
group: props.row,
|
||||
target: $event.currentTarget,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GroupNameField',
|
||||
functional: true,
|
||||
props: {
|
||||
row: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
column: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import UserGroupsField from '@baserow_premium/components/admin/users/fields/UserGroupsField'
|
||||
|
||||
export default {
|
||||
name: 'GroupUsersField',
|
||||
extends: UserGroupsField,
|
||||
data() {
|
||||
return {
|
||||
eventName: 'show-hidden-groups',
|
||||
nameKey: 'email',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<Modal>
|
||||
<h2 class="box__title">Delete {{ group.name }}</h2>
|
||||
<Error :error="error"></Error>
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to delete the group:
|
||||
<strong>{{ group.name }}</strong
|
||||
>?
|
||||
</p>
|
||||
<p>
|
||||
The group will be permanently deleted, including the related
|
||||
applications. It is not possible to undo this action.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<div class="align-right">
|
||||
<a
|
||||
class="button button--large button--error button--overflow"
|
||||
:class="{ 'button--loading': loading }"
|
||||
:disabled="loading"
|
||||
:title="group.name"
|
||||
@click.prevent="deleteGroup()"
|
||||
>
|
||||
Delete group {{ group.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import GroupsAdminService from '@baserow_premium/services/admin/groups'
|
||||
|
||||
export default {
|
||||
name: 'DeleteGroupModal',
|
||||
mixins: [modal, error],
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deleteGroup() {
|
||||
this.hideError()
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await GroupsAdminService(this.$client).delete(this.group.id)
|
||||
this.$emit('group-deleted', this.group.id)
|
||||
this.hide()
|
||||
} catch (error) {
|
||||
this.handleError(error, 'group')
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -5,11 +5,11 @@
|
|||
:service="service"
|
||||
row-id-key="id"
|
||||
@edit-user="displayEditUserContext"
|
||||
@show-group="displayHiddenGroups"
|
||||
@show-hidden-groups="displayHiddenGroups"
|
||||
@row-context="onRowContext"
|
||||
>
|
||||
<template #header>
|
||||
<div class="crudtable__header-title">User Settings</div>
|
||||
<div class="crudtable__header-title">All users</div>
|
||||
</template>
|
||||
<template #menus="slotProps">
|
||||
<EditUserContext
|
||||
|
@ -21,26 +21,26 @@
|
|||
</EditUserContext>
|
||||
<HiddenGroupsContext
|
||||
ref="hiddenGroupsContext"
|
||||
:hidden-groups="hiddenGroups"
|
||||
:hidden-values="hiddenGroups"
|
||||
></HiddenGroupsContext>
|
||||
</template>
|
||||
</CrudTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAdminService from '@baserow_premium/services/userAdmin'
|
||||
import UsernameField from '@baserow_premium/components/admin/user/fields/UsernameField'
|
||||
import UserGroupsField from '@baserow_premium/components/admin/user/fields/UserGroupsField'
|
||||
import UserAdminService from '@baserow_premium/services/admin/users'
|
||||
import UsernameField from '@baserow_premium/components/admin/users/fields/UsernameField'
|
||||
import UserGroupsField from '@baserow_premium/components/admin/users/fields/UserGroupsField'
|
||||
import CrudTable from '@baserow_premium/components/crud_table/CrudTable'
|
||||
import SimpleField from '@baserow_premium/components/crud_table/fields/SimpleField'
|
||||
import LocalDateField from '@baserow_premium/components/crud_table/fields/LocalDateField'
|
||||
import ActiveField from '@baserow_premium/components/admin/user/fields/ActiveField'
|
||||
import EditUserContext from '@baserow_premium/components/admin/user/contexts/EditUserContext'
|
||||
import HiddenGroupsContext from '@baserow_premium/components/admin/user/contexts/HiddenGroupsContext'
|
||||
import CrudTableColumn from '@baserow_premium/crud_table/CrudTableColumn'
|
||||
import ActiveField from '@baserow_premium/components/admin/users/fields/ActiveField'
|
||||
import EditUserContext from '@baserow_premium/components/admin/users/contexts/EditUserContext'
|
||||
import HiddenGroupsContext from '@baserow_premium/components/admin/users/contexts/HiddenGroupsContext'
|
||||
import CrudTableColumn from '@baserow_premium/crud_table/crudTableColumn'
|
||||
|
||||
export default {
|
||||
name: 'UserAdminTable',
|
||||
name: 'UsersAdminTable',
|
||||
components: {
|
||||
HiddenGroupsContext,
|
||||
CrudTable,
|
||||
|
@ -107,8 +107,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
displayEditUserContext(event) {
|
||||
const action = event.user.id === this.editUser.id ? 'toggle' : 'show'
|
||||
this.editUser = event.user
|
||||
this.$refs.editUserContext.show(event.target, 'bottom', 'left', 4)
|
||||
this.$refs.editUserContext[action](event.target, 'bottom', 'left', 4)
|
||||
},
|
||||
onRowContext({ row, event }) {
|
||||
this.displayEditUserContext({
|
||||
|
@ -120,8 +121,10 @@ export default {
|
|||
})
|
||||
},
|
||||
displayHiddenGroups(event) {
|
||||
this.hiddenGroups = event.hiddenGroups
|
||||
this.$refs.hiddenGroupsContext.show(event.target, 'bottom', 'left', 4)
|
||||
const action =
|
||||
this.hiddenGroups === event.hiddenValues ? 'toggle' : 'show'
|
||||
this.hiddenGroups = event.hiddenValues
|
||||
this.$refs.hiddenGroupsContext[action](event.target, 'bottom', 'left', 4)
|
||||
},
|
||||
},
|
||||
}
|
|
@ -64,12 +64,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ChangePasswordModal from '@baserow_premium/components/admin/user/modals/ChangeUserPasswordModal'
|
||||
import context from '@baserow/modules/core/mixins/context'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/user/modals/DeleteUserModal'
|
||||
import EditUserModal from '@baserow_premium/components/admin/user/modals/EditUserModal'
|
||||
import UserAdminService from '@baserow_premium/services/userAdmin'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import ChangePasswordModal from '@baserow_premium/components/admin/users/modals/ChangeUserPasswordModal'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/users/modals/DeleteUserModal'
|
||||
import EditUserModal from '@baserow_premium/components/admin/users/modals/EditUserModal'
|
||||
import UserAdminService from '@baserow_premium/services/admin/users'
|
||||
|
||||
export default {
|
||||
name: 'EditUserContext',
|
|
@ -2,11 +2,11 @@
|
|||
<Context>
|
||||
<ul class="context__menu">
|
||||
<li
|
||||
v-for="group in hiddenGroups"
|
||||
v-for="group in hiddenValues"
|
||||
:key="'hidden-admin-row-group' + group.id"
|
||||
class="user-admin-group__dropdown-item"
|
||||
>
|
||||
{{ group.name }}
|
||||
{{ group[nameKey] }}
|
||||
<i
|
||||
v-if="group.permissions == 'ADMIN'"
|
||||
v-tooltip="'is group admin'"
|
||||
|
@ -24,10 +24,15 @@ export default {
|
|||
name: 'HiddenGroupsContext',
|
||||
mixins: [context],
|
||||
props: {
|
||||
hiddenGroups: {
|
||||
hiddenValues: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nameKey: 'name',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -11,7 +11,7 @@
|
|||
order: index,
|
||||
}"
|
||||
>
|
||||
{{ group.name }}
|
||||
{{ group[nameKey] }}
|
||||
<i
|
||||
v-if="group.permissions == 'ADMIN'"
|
||||
v-tooltip="'is group admin'"
|
||||
|
@ -52,6 +52,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
eventName: 'show-hidden-groups',
|
||||
nameKey: 'name',
|
||||
overflowing: false,
|
||||
numHiddenGroups: 0,
|
||||
renderContext: false,
|
||||
|
@ -81,8 +83,8 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
showContext(event) {
|
||||
this.$emit('show-group', {
|
||||
hiddenGroups: this.hiddenGroups,
|
||||
this.$emit(this.eventName, {
|
||||
hiddenValues: this.hiddenGroups,
|
||||
target: event.currentTarget,
|
||||
time: Date.now(),
|
||||
})
|
|
@ -12,8 +12,8 @@
|
|||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import UserAdminService from '@baserow_premium/services/userAdmin'
|
||||
import ChangePasswordForm from '@baserow_premium/components/admin/user/forms/ChangePasswordForm'
|
||||
import UserAdminService from '@baserow_premium/services/admin/users'
|
||||
import ChangePasswordForm from '@baserow_premium/components/admin/users/forms/ChangePasswordForm'
|
||||
|
||||
export default {
|
||||
name: 'ChangePasswordModal',
|
|
@ -39,7 +39,7 @@
|
|||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import UserAdminService from '@baserow_premium/services/userAdmin'
|
||||
import UserAdminService from '@baserow_premium/services/admin/users'
|
||||
|
||||
export default {
|
||||
name: 'DeleteUserModal',
|
|
@ -29,9 +29,9 @@
|
|||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import UserAdminService from '@baserow_premium/services/userAdmin'
|
||||
import UserForm from '@baserow_premium/components/admin/user/forms/UserForm'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/user/modals/DeleteUserModal'
|
||||
import UserAdminService from '@baserow_premium/services/admin/users'
|
||||
import UserForm from '@baserow_premium/components/admin/users/forms/UserForm'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/users/modals/DeleteUserModal'
|
||||
|
||||
export default {
|
||||
name: 'EditUserModal',
|
|
@ -70,7 +70,7 @@
|
|||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import CrudTableSearch from '@baserow_premium/components/crud_table/CrudTableSearch'
|
||||
import Paginator from '@baserow/modules/core/components/Paginator'
|
||||
import CrudTableColumn from '@baserow_premium/crud_table/CrudTableColumn'
|
||||
import CrudTableColumn from '@baserow_premium/crud_table/crudTableColumn'
|
||||
|
||||
/**
|
||||
* This component is a generic wrapper for a basic crud service which displays its
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
export default (client) => {
|
||||
export default (client, baseUrl) => {
|
||||
return {
|
||||
/**
|
||||
* @param {number} page The page number to fetch.
|
||||
* @param {{searchQuery}} searchQuery A search term to filter users by username.
|
||||
* @param {{searchQuery}} searchQuery A search term to filter results.
|
||||
* @param {[{direction:string, key:string}]} sorts An ordered list of sorts to
|
||||
* apply to the users.
|
||||
* apply to the results.
|
||||
*/
|
||||
fetchPage(page, searchQuery, sorts) {
|
||||
const params = { page }
|
||||
|
@ -19,13 +19,7 @@ export default (client) => {
|
|||
})
|
||||
.join(',')
|
||||
}
|
||||
return client.get(`/admin/user/`, { params })
|
||||
},
|
||||
update(userId, values) {
|
||||
return client.patch(`/admin/user/${userId}/`, values)
|
||||
},
|
||||
delete(userId) {
|
||||
return client.delete(`/admin/user/${userId}/`)
|
||||
return client.get(baseUrl, { params })
|
||||
},
|
||||
}
|
||||
}
|
|
@ -21,7 +21,9 @@
|
|||
<div class="admin-dashboard__numbers-value">
|
||||
{{ data.total_groups }}
|
||||
</div>
|
||||
<div class="admin-dashboard__numbers-percentage"></div>
|
||||
<div class="admin-dashboard__numbers-percentage">
|
||||
<nuxt-link :to="{ name: 'admin-groups' }">view all</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-dashboard__numbers">
|
||||
<div class="admin-dashboard__numbers-name">
|
||||
|
@ -173,7 +175,7 @@
|
|||
|
||||
<script>
|
||||
import ActiveUsers from '@baserow_premium/components/admin/dashboard/charts/ActiveUsers'
|
||||
import AdminDashboardService from '@baserow_premium/services/adminDashboard'
|
||||
import AdminDashboardService from '@baserow_premium/services/admin/dashboard'
|
||||
|
||||
export default {
|
||||
components: { ActiveUsers },
|
||||
|
@ -183,6 +185,9 @@ export default {
|
|||
return {
|
||||
loading: true,
|
||||
data: {
|
||||
total_users: 0,
|
||||
total_groups: 0,
|
||||
total_applications: 0,
|
||||
new_users_last_24_hours: 0,
|
||||
new_users_last_7_days: 0,
|
||||
new_users_last_30_days: 0,
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<GroupsAdminTable></GroupsAdminTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupsAdminTable from '@baserow_premium/components/admin/groups/GroupsAdminTable'
|
||||
|
||||
export default {
|
||||
components: { GroupsAdminTable },
|
||||
layout: 'app',
|
||||
middleware: 'staff',
|
||||
}
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<UserAdminTable></UserAdminTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAdminTable from '@baserow_premium/components/admin/user/UserAdminTable'
|
||||
|
||||
export default {
|
||||
components: { UserAdminTable },
|
||||
layout: 'app',
|
||||
middleware: 'staff',
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<UsersAdminTable></UsersAdminTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UsersAdminTable from '@baserow_premium/components/admin/users/UsersAdminTable'
|
||||
|
||||
export default {
|
||||
components: { UsersAdminTable },
|
||||
layout: 'app',
|
||||
middleware: 'staff',
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,13 @@
|
|||
import { PremPlugin } from '@baserow_premium/plugins'
|
||||
import { DashboardType, UsersAdminType } from '@baserow_premium/adminTypes'
|
||||
import { PremiumPlugin } from '@baserow_premium/plugins'
|
||||
import {
|
||||
DashboardType,
|
||||
UsersAdminType,
|
||||
GroupsAdminType,
|
||||
} from '@baserow_premium/adminTypes'
|
||||
|
||||
export default ({ app }) => {
|
||||
app.$registry.register('plugin', new PremPlugin())
|
||||
app.$registry.register('plugin', new PremiumPlugin())
|
||||
app.$registry.register('admin', new DashboardType())
|
||||
app.$registry.register('admin', new UsersAdminType())
|
||||
app.$registry.register('admin', new GroupsAdminType())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BaserowPlugin } from '@baserow/modules/core/plugins'
|
||||
|
||||
export class PremPlugin extends BaserowPlugin {
|
||||
export class PremiumPlugin extends BaserowPlugin {
|
||||
static getType() {
|
||||
return 'plugin'
|
||||
}
|
||||
|
|
|
@ -9,6 +9,11 @@ export const routes = [
|
|||
{
|
||||
name: 'admin-users',
|
||||
path: '/admin/users',
|
||||
component: path.resolve(__dirname, 'pages/admin/userAdmin.vue'),
|
||||
component: path.resolve(__dirname, 'pages/admin/users.vue'),
|
||||
},
|
||||
{
|
||||
name: 'admin-groups',
|
||||
path: '/admin/groups',
|
||||
component: path.resolve(__dirname, 'pages/admin/groups.vue'),
|
||||
},
|
||||
]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import baseService from '@baserow_premium/crud_table/baseService'
|
||||
|
||||
export default (client) => {
|
||||
return Object.assign(baseService(client, '/admin/groups/'), {
|
||||
delete(groupId) {
|
||||
return client.delete(`/admin/groups/${groupId}/`)
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import baseService from '@baserow_premium/crud_table/baseService'
|
||||
|
||||
export default (client) => {
|
||||
return Object.assign(baseService(client, '/admin/users/'), {
|
||||
update(userId, values) {
|
||||
return client.patch(`/admin/users/${userId}/`, values)
|
||||
},
|
||||
delete(userId) {
|
||||
return client.delete(`/admin/users/${userId}/`)
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import EditUserContext from '@baserow_premium/components/admin/user/contexts/EditUserContext'
|
||||
import EditUserContext from '@baserow_premium/components/admin/users/contexts/EditUserContext'
|
||||
import Error from '@baserow/modules/core/components/Error'
|
||||
import ChangeUserPasswordModal from '@baserow_premium/components/admin/user/modals/ChangeUserPasswordModal'
|
||||
import EditUserModal from '@baserow_premium/components/admin/user/modals/EditUserModal'
|
||||
import ChangeUserPasswordModal from '@baserow_premium/components/admin/users/modals/ChangeUserPasswordModal'
|
||||
import EditUserModal from '@baserow_premium/components/admin/users/modals/EditUserModal'
|
||||
import CrudTableSearchContext from '@baserow_premium/components/crud_table/CrudTableSearchContext'
|
||||
import CrudTableSearch from '@baserow_premium/components/crud_table/CrudTableSearch'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/user/modals/DeleteUserModal'
|
||||
import DeleteUserModal from '@baserow_premium/components/admin/users/modals/DeleteUserModal'
|
||||
|
||||
export default class UserAdminUserHelpers {
|
||||
constructor(userAdminComponent) {
|
||||
|
|
8
premium/web-frontend/test/fixtures/user.js
vendored
8
premium/web-frontend/test/fixtures/user.js
vendored
|
@ -39,22 +39,22 @@ export function createUsersForAdmin(
|
|||
if (sorts !== null) {
|
||||
params.sorts = sorts
|
||||
}
|
||||
mock.onGet(`/admin/user/`, { params }).reply(200, {
|
||||
mock.onGet(`/admin/users/`, { params }).reply(200, {
|
||||
count: count === null ? users.length : count,
|
||||
results: users,
|
||||
})
|
||||
}
|
||||
|
||||
export function expectUserDeleted(mock, userId) {
|
||||
mock.onDelete(`/admin/user/${userId}/`).reply(200)
|
||||
mock.onDelete(`/admin/users/${userId}/`).reply(200)
|
||||
}
|
||||
|
||||
export function expectUserUpdated(mock, user, changes) {
|
||||
mock
|
||||
.onPatch(`/admin/user/${user.id}/`, changes)
|
||||
.onPatch(`/admin/users/${user.id}/`, changes)
|
||||
.reply(200, Object.assign({}, user, changes))
|
||||
}
|
||||
|
||||
export function expectUserUpdatedRespondsWithError(mock, user, error) {
|
||||
mock.onPatch(`/admin/user/${user.id}/`).reply(500, error)
|
||||
mock.onPatch(`/admin/users/${user.id}/`).reply(500, error)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TestApp } from '@baserow/test/helpers/testApp'
|
||||
import UserAdminTable from '@baserow_premium/components/admin/user/UserAdminTable'
|
||||
import UsersAdminTable from '@baserow_premium/components/admin/users/UsersAdminTable'
|
||||
import moment from 'moment'
|
||||
import flushPromises from 'flush-promises'
|
||||
import UserAdminUserHelpers from '../../../../fixtures/uiHelpers'
|
||||
|
@ -406,7 +406,7 @@ describe('User Admin Component Tests', () => {
|
|||
mockPremiumServer.thereAreUsers([firstPageUser], 1, { count: 150 })
|
||||
mockPremiumServer.thereAreUsers([secondPageUser], 2, { count: 150 })
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
|
||||
expect(ui.getSingleRowUsernameText()).toContain(firstPageUser.username)
|
||||
|
@ -436,7 +436,7 @@ describe('User Admin Component Tests', () => {
|
|||
})
|
||||
mockPremiumServer.thereAreUsers([secondPageUser], 2, { count: 150 })
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
|
||||
expect(ui.getSingleRowUsernameText()).toContain(firstPageUser.username)
|
||||
|
@ -459,7 +459,7 @@ describe('User Admin Component Tests', () => {
|
|||
mockPremiumServer.thereAreUsers([firstUser, secondUser], 1)
|
||||
mockPremiumServer.thereAreUsers([firstUser], 1, { search: 'firstUser' })
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
|
||||
const cells = ui.findCells(14)
|
||||
|
@ -490,7 +490,7 @@ describe('User Admin Component Tests', () => {
|
|||
sorts: '+username',
|
||||
})
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
|
||||
const cells = ui.findCells(14)
|
||||
|
@ -528,7 +528,7 @@ describe('User Admin Component Tests', () => {
|
|||
})
|
||||
mockPremiumServer.thereAreUsers([firstUser, secondUser, thirdUser], 1)
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
|
||||
let usernameCellsText = ui.findUsernameColumnCellsText()
|
||||
|
@ -632,7 +632,7 @@ describe('User Admin Component Tests', () => {
|
|||
const user = mockPremiumServer.aUser(userSetup)
|
||||
mockPremiumServer.thereAreUsers([user], 1)
|
||||
|
||||
const userAdmin = await testApp.mount(UserAdminTable, {})
|
||||
const userAdmin = await testApp.mount(UsersAdminTable, {})
|
||||
const ui = new UserAdminUserHelpers(userAdmin)
|
||||
return { user, userAdmin, ui }
|
||||
}
|
|
@ -28,7 +28,7 @@ export class AdminType extends Registerable {
|
|||
* The order value used to sort admin types in the sidebar menu.
|
||||
*/
|
||||
getOrder() {
|
||||
return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
getRouteName() {
|
||||
|
@ -82,4 +82,8 @@ export class SettingsAdminType extends AdminType {
|
|||
getRouteName() {
|
||||
return 'admin-settings'
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 9999
|
||||
}
|
||||
}
|
||||
|
|
|
@ -259,7 +259,7 @@ export default {
|
|||
sortedAdminTypes() {
|
||||
return Object.values(this.adminTypes)
|
||||
.slice()
|
||||
.sort((x) => x.getOrder())
|
||||
.sort((a, b) => a.getOrder() - b.getOrder())
|
||||
},
|
||||
/**
|
||||
* Indicates whether the current user is visiting an admin page.
|
||||
|
|
Loading…
Add table
Reference in a new issue