mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-04 13:15:24 +00:00
Realtime user and group user updates
This commit is contained in:
parent
9dfb3c2330
commit
95c0a54c12
23 changed files with 1254 additions and 97 deletions
.gitignorechangelog.md
backend
src/baserow
api
groups
user
core
ws
tests/baserow
docs/apis
web-frontend
modules/core
test/unit/core/store
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -127,3 +127,5 @@ junit.xml
|
|||
!plugin-boilerplate/{{ cookiecutter.project_slug }}/.env
|
||||
|
||||
field-diagrams/
|
||||
|
||||
*.http
|
|
@ -324,7 +324,10 @@ class AcceptGroupInvitationView(APIView):
|
|||
group_user = CoreHandler().accept_group_invitation(
|
||||
request.user, group_invitation
|
||||
)
|
||||
return Response(GroupUserGroupSerializer(group_user).data)
|
||||
groupuser_group = (
|
||||
CoreHandler().get_groupuser_group_queryset().get(id=group_user.id)
|
||||
)
|
||||
return Response(GroupUserGroupSerializer(groupuser_group).data)
|
||||
|
||||
|
||||
class RejectGroupInvitationView(APIView):
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
from drf_spectacular.plumbing import build_object_type
|
||||
|
||||
group_user_schema = build_object_type(
|
||||
{
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"description": "The order of the group, lowest first.",
|
||||
"example": 0,
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The unique identifier of the group.",
|
||||
"example": 1,
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name given to the group.",
|
||||
"example": "Bram's group",
|
||||
},
|
||||
}
|
||||
)
|
|
@ -2,9 +2,14 @@ from rest_framework import serializers
|
|||
|
||||
from baserow.core.models import Group
|
||||
|
||||
from .users.serializers import GroupUserGroupSerializer
|
||||
from .users.serializers import GroupUserGroupSerializer, GroupUserSerializer
|
||||
|
||||
__all__ = ["GroupUserGroupSerializer", "GroupSerializer", "OrderGroupsSerializer"]
|
||||
__all__ = [
|
||||
"GroupUserGroupSerializer",
|
||||
"GroupSerializer",
|
||||
"OrderGroupsSerializer",
|
||||
"GroupUserSerializer",
|
||||
]
|
||||
|
||||
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -39,22 +39,36 @@ class GroupUserSerializer(serializers.ModelSerializer):
|
|||
return object.user.email
|
||||
|
||||
|
||||
class GroupUserGroupSerializer(serializers.ModelSerializer):
|
||||
class GroupUserGroupSerializer(serializers.Serializer):
|
||||
"""
|
||||
This serializers returns all the fields that the GroupSerializer has, but also
|
||||
some user specific values related to the group user relation.
|
||||
This serializers includes relevant fields of the Group model, but also
|
||||
some GroupUser specific fields related to the group user relation.
|
||||
|
||||
Additionally, the list of users are included for each group.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = GroupUser
|
||||
fields = ("order", "permissions")
|
||||
# Group fields
|
||||
id = serializers.IntegerField(
|
||||
source="group.id", read_only=True, help_text="Group id."
|
||||
)
|
||||
name = serializers.CharField(
|
||||
source="group.name", read_only=True, help_text="Group name."
|
||||
)
|
||||
users = GroupUserSerializer(
|
||||
many=True,
|
||||
source="group.groupuser_set",
|
||||
required=False,
|
||||
read_only=True,
|
||||
help_text="List of all group users.",
|
||||
)
|
||||
|
||||
def to_representation(self, instance):
|
||||
from baserow.api.groups.serializers import GroupSerializer
|
||||
|
||||
data = super().to_representation(instance)
|
||||
data.update(GroupSerializer(instance.group).data)
|
||||
return data
|
||||
# GroupUser fields
|
||||
order = serializers.IntegerField(
|
||||
read_only=True, help_text="The requesting user's order within the group users."
|
||||
)
|
||||
permissions = serializers.CharField(
|
||||
read_only=True, help_text="The requesting user's permissions for the group."
|
||||
)
|
||||
|
||||
|
||||
class UpdateGroupUserSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -23,11 +23,7 @@ from baserow.core.exceptions import (
|
|||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import GroupUser
|
||||
|
||||
from .serializers import (
|
||||
GroupUserGroupSerializer,
|
||||
GroupUserSerializer,
|
||||
UpdateGroupUserSerializer,
|
||||
)
|
||||
from .serializers import GroupUserSerializer, UpdateGroupUserSerializer
|
||||
|
||||
|
||||
class GroupUsersView(APIView):
|
||||
|
@ -95,7 +91,7 @@ class GroupUserView(APIView):
|
|||
),
|
||||
request=UpdateGroupUserSerializer,
|
||||
responses={
|
||||
200: GroupUserGroupSerializer,
|
||||
200: GroupUserSerializer,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
|
@ -123,7 +119,7 @@ class GroupUserView(APIView):
|
|||
base_queryset=GroupUser.objects.select_for_update(of=("self",)),
|
||||
)
|
||||
group_user = CoreHandler().update_group_user(request.user, group_user, **data)
|
||||
return Response(GroupUserGroupSerializer(group_user).data)
|
||||
return Response(GroupUserSerializer(group_user).data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.db import transaction
|
||||
|
||||
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
|
||||
from drf_spectacular.plumbing import build_array_type
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
@ -34,11 +33,9 @@ from baserow.core.exceptions import (
|
|||
UserNotInGroup,
|
||||
)
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import GroupUser
|
||||
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
|
||||
|
||||
from .errors import ERROR_GROUP_USER_IS_LAST_ADMIN
|
||||
from .schemas import group_user_schema
|
||||
from .serializers import GroupSerializer, OrderGroupsSerializer
|
||||
|
||||
|
||||
|
@ -56,13 +53,15 @@ class GroupsView(APIView):
|
|||
"are custom for each user. The order is configurable via the "
|
||||
"**order_groups** endpoint."
|
||||
),
|
||||
responses={200: build_array_type(group_user_schema)},
|
||||
responses={200: GroupUserGroupSerializer(many=True)},
|
||||
)
|
||||
def get(self, request):
|
||||
"""Responds with a list of serialized groups where the user is part of."""
|
||||
|
||||
groups = GroupUser.objects.filter(user=request.user).select_related("group")
|
||||
serializer = GroupUserGroupSerializer(groups, many=True)
|
||||
groupuser_groups = (
|
||||
CoreHandler().get_groupuser_group_queryset().filter(user=request.user)
|
||||
)
|
||||
serializer = GroupUserGroupSerializer(groupuser_groups, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -75,7 +74,7 @@ class GroupsView(APIView):
|
|||
"created via other endpoints."
|
||||
),
|
||||
request=GroupSerializer,
|
||||
responses={200: group_user_schema},
|
||||
responses={200: GroupUserGroupSerializer},
|
||||
)
|
||||
@transaction.atomic
|
||||
@validate_body(GroupSerializer)
|
||||
|
@ -160,7 +159,7 @@ class GroupView(APIView):
|
|||
),
|
||||
request=GroupSerializer,
|
||||
responses={
|
||||
200: group_user_schema,
|
||||
204: None,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
|
|
|
@ -48,6 +48,20 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class PublicUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer that exposes only fields that can be shared
|
||||
about the user for the whole group.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("id", "username", "first_name")
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(min_length=2, max_length=150)
|
||||
email = serializers.EmailField(
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Q, QuerySet
|
||||
from django.db.models import Count, Prefetch, Q, QuerySet
|
||||
from django.utils import translation
|
||||
|
||||
from itsdangerous import URLSafeSerializer
|
||||
|
@ -153,6 +153,22 @@ class CoreHandler:
|
|||
|
||||
return group
|
||||
|
||||
def get_groupuser_group_queryset(self) -> QuerySet[GroupUser]:
|
||||
"""
|
||||
Returns GroupUser queryset that will prefetch groups and their users.
|
||||
"""
|
||||
|
||||
groupusers_with_user_and_profile = GroupUser.objects.select_related(
|
||||
"user"
|
||||
).select_related("user__profile")
|
||||
groupuser_groups = GroupUser.objects.select_related("group").prefetch_related(
|
||||
Prefetch(
|
||||
"group__groupuser_set",
|
||||
queryset=groupusers_with_user_and_profile,
|
||||
)
|
||||
)
|
||||
return groupuser_groups
|
||||
|
||||
def create_group(self, user: User, name: str) -> GroupUser:
|
||||
"""
|
||||
Creates a new group for an existing user.
|
||||
|
@ -243,7 +259,10 @@ class CoreHandler:
|
|||
group_user_id = group_user.id
|
||||
group_user.delete()
|
||||
group_user_deleted.send(
|
||||
self, group_user_id=group_user_id, group_user=group_user, user=user
|
||||
self,
|
||||
group_user_id=group_user_id,
|
||||
group_user=group_user,
|
||||
user=user,
|
||||
)
|
||||
|
||||
def delete_group_by_id(self, user: AbstractUser, group_id: int):
|
||||
|
@ -394,7 +413,10 @@ class CoreHandler:
|
|||
group_user.delete()
|
||||
|
||||
group_user_deleted.send(
|
||||
self, group_user_id=group_user_id, group_user=group_user, user=user
|
||||
self,
|
||||
group_user_id=group_user_id,
|
||||
group_user=group_user,
|
||||
user=user,
|
||||
)
|
||||
|
||||
def get_group_invitation_signer(self):
|
||||
|
|
|
@ -6,6 +6,11 @@ before_user_deleted = Signal()
|
|||
|
||||
before_group_deleted = Signal()
|
||||
|
||||
user_updated = Signal()
|
||||
user_deleted = Signal()
|
||||
user_restored = Signal()
|
||||
user_permanently_deleted = Signal()
|
||||
|
||||
group_created = Signal()
|
||||
group_updated = Signal()
|
||||
group_deleted = Signal()
|
||||
|
|
|
@ -19,9 +19,15 @@ from baserow.core.exceptions import (
|
|||
GroupInvitationEmailMismatch,
|
||||
)
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import Group, Template, UserLogEntry, UserProfile
|
||||
from baserow.core.models import Group, GroupUser, Template, UserLogEntry, UserProfile
|
||||
from baserow.core.registries import plugin_registry
|
||||
from baserow.core.signals import before_user_deleted
|
||||
from baserow.core.signals import (
|
||||
before_user_deleted,
|
||||
user_deleted,
|
||||
user_permanently_deleted,
|
||||
user_restored,
|
||||
user_updated,
|
||||
)
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
|
||||
from .emails import (
|
||||
|
@ -189,7 +195,8 @@ class UserHandler:
|
|||
language: Optional[str] = None,
|
||||
) -> AbstractUser:
|
||||
"""
|
||||
Updates the user's account editable properties
|
||||
Updates the user's account editable properties. Handles the scenario
|
||||
when a user edits his own account.
|
||||
|
||||
:param user: The user instance to update.
|
||||
:param first_name: The new user first name.
|
||||
|
@ -205,6 +212,8 @@ class UserHandler:
|
|||
user.profile.language = language
|
||||
user.profile.save()
|
||||
|
||||
user_updated.send(self, performed_by=user, user=user)
|
||||
|
||||
return user
|
||||
|
||||
def get_reset_password_signer(self) -> URLSafeTimedSerializer:
|
||||
|
@ -374,6 +383,8 @@ class UserHandler:
|
|||
email = AccountDeletionScheduled(user, days_left, to=[user.email])
|
||||
email.send()
|
||||
|
||||
user_deleted.send(self, performed_by=user, user=user)
|
||||
|
||||
def cancel_user_deletion(self, user: AbstractUser):
|
||||
"""
|
||||
Cancels a previously scheduled user account deletion. This action send an email
|
||||
|
@ -389,6 +400,8 @@ class UserHandler:
|
|||
email = AccountDeletionCanceled(user, to=[user.email])
|
||||
email.send()
|
||||
|
||||
user_restored.send(self, performed_by=user, user=user)
|
||||
|
||||
def delete_expired_users(self, grace_delay: Optional[timedelta] = None):
|
||||
"""
|
||||
Executes all previously scheduled user account deletions for which
|
||||
|
@ -416,9 +429,14 @@ class UserHandler:
|
|||
profile__to_be_deleted=True, last_login__lt=limit_date
|
||||
)
|
||||
|
||||
deleted_user_info = [
|
||||
(u.username, u.email, u.profile.language) for u in users_to_delete.all()
|
||||
]
|
||||
group_users = GroupUser.objects.filter(user__in=users_to_delete)
|
||||
|
||||
deleted_user_info = []
|
||||
for u in users_to_delete.all():
|
||||
group_ids = [gu.group_id for gu in group_users if gu.user_id == u.id]
|
||||
deleted_user_info.append(
|
||||
(u.id, u.username, u.email, u.profile.language, group_ids)
|
||||
)
|
||||
|
||||
# A group need to be deleted if there was an admin before and there is no
|
||||
# *active* admin after the users deletion.
|
||||
|
@ -447,7 +465,8 @@ class UserHandler:
|
|||
TrashHandler.permanently_delete(group)
|
||||
users_to_delete.delete()
|
||||
|
||||
for (username, email, language) in deleted_user_info:
|
||||
for (id, username, email, language, group_ids) in deleted_user_info:
|
||||
with translation.override(language):
|
||||
email = AccountDeleted(username, to=[email])
|
||||
email.send()
|
||||
user_permanently_deleted.send(self, user_id=id, group_ids=group_ids)
|
||||
|
|
|
@ -2,10 +2,68 @@ from django.db import transaction
|
|||
from django.dispatch import receiver
|
||||
|
||||
from baserow.api.applications.serializers import get_application_serializer
|
||||
from baserow.api.groups.serializers import GroupSerializer, GroupUserGroupSerializer
|
||||
from baserow.api.groups.serializers import (
|
||||
GroupSerializer,
|
||||
GroupUserGroupSerializer,
|
||||
GroupUserSerializer,
|
||||
)
|
||||
from baserow.api.user.serializers import PublicUserSerializer
|
||||
from baserow.core import signals
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import GroupUser
|
||||
|
||||
from .tasks import broadcast_to_group, broadcast_to_users
|
||||
from .tasks import broadcast_to_group, broadcast_to_groups, broadcast_to_users
|
||||
|
||||
|
||||
@receiver(signals.user_updated)
|
||||
def user_updated(sender, performed_by, user, **kwargs):
|
||||
group_ids = list(
|
||||
GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
|
||||
)
|
||||
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_groups.delay(
|
||||
group_ids,
|
||||
{"type": "user_updated", "user": PublicUserSerializer(user).data},
|
||||
getattr(performed_by, "web_socket_id", None),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.user_deleted)
|
||||
def user_deleted(sender, performed_by, user, **kwargs):
|
||||
group_ids = list(
|
||||
GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
|
||||
)
|
||||
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_groups.delay(
|
||||
group_ids, {"type": "user_deleted", "user": PublicUserSerializer(user).data}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.user_restored)
|
||||
def user_restored(sender, performed_by, user, **kwargs):
|
||||
group_ids = list(
|
||||
GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
|
||||
)
|
||||
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_groups.delay(
|
||||
group_ids,
|
||||
{"type": "user_restored", "user": PublicUserSerializer(user).data},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.user_permanently_deleted)
|
||||
def user_permanently_deleted(sender, user_id, group_ids, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_groups.delay(
|
||||
group_ids, {"type": "user_permanently_deleted", "user_id": user_id}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.group_created)
|
||||
|
@ -45,36 +103,32 @@ def group_deleted(sender, group_id, group, group_users, user=None, **kwargs):
|
|||
)
|
||||
|
||||
|
||||
@receiver(signals.group_user_updated)
|
||||
def group_user_updated(sender, group_user, user, **kwargs):
|
||||
@receiver(signals.group_user_added)
|
||||
def group_user_added(sender, group_user, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_users.delay(
|
||||
[group_user.user_id],
|
||||
lambda: broadcast_to_group.delay(
|
||||
group_user.group_id,
|
||||
{
|
||||
"type": "group_updated",
|
||||
"type": "group_user_added",
|
||||
"id": group_user.id,
|
||||
"group_id": group_user.group_id,
|
||||
"group": GroupUserGroupSerializer(group_user).data,
|
||||
"group_user": GroupUserSerializer(group_user).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(signals.group_restored)
|
||||
def group_restored(sender, group_user, user, **kwargs):
|
||||
@receiver(signals.group_user_updated)
|
||||
def group_user_updated(sender, group_user, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_users.delay(
|
||||
[group_user.user_id],
|
||||
lambda: broadcast_to_group.delay(
|
||||
group_user.group_id,
|
||||
{
|
||||
"type": "group_restored",
|
||||
"type": "group_user_updated",
|
||||
"id": group_user.id,
|
||||
"group_id": group_user.group_id,
|
||||
"group": GroupUserGroupSerializer(group_user).data,
|
||||
"applications": [
|
||||
get_application_serializer(application).data
|
||||
for application in group_user.group.application_set.select_related(
|
||||
"content_type", "group"
|
||||
).all()
|
||||
],
|
||||
"group_user": GroupUserSerializer(group_user).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
|
@ -82,11 +136,47 @@ def group_restored(sender, group_user, user, **kwargs):
|
|||
|
||||
|
||||
@receiver(signals.group_user_deleted)
|
||||
def group_user_deleted(sender, group_user, user, **kwargs):
|
||||
def group_user_deleted(sender, group_user_id, group_user, user, **kwargs):
|
||||
def broadcast_to_group_and_removed_user():
|
||||
payload = {
|
||||
"type": "group_user_deleted",
|
||||
"id": group_user_id,
|
||||
"group_id": group_user.group_id,
|
||||
"group_user": GroupUserSerializer(group_user).data,
|
||||
}
|
||||
broadcast_to_group.delay(
|
||||
group_user.group_id,
|
||||
payload,
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
broadcast_to_users.delay(
|
||||
[group_user.user_id],
|
||||
payload,
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
|
||||
transaction.on_commit(broadcast_to_group_and_removed_user)
|
||||
|
||||
|
||||
@receiver(signals.group_restored)
|
||||
def group_restored(sender, group_user, user, **kwargs):
|
||||
groupuser_groups = (
|
||||
CoreHandler().get_groupuser_group_queryset().get(id=group_user.id)
|
||||
)
|
||||
transaction.on_commit(
|
||||
lambda: broadcast_to_users.delay(
|
||||
[group_user.user_id],
|
||||
{"type": "group_deleted", "group_id": group_user.group_id},
|
||||
{
|
||||
"type": "group_restored",
|
||||
"group_id": group_user.group_id,
|
||||
"group": GroupUserGroupSerializer(groupuser_groups).data,
|
||||
"applications": [
|
||||
get_application_serializer(application).data
|
||||
for application in group_user.group.application_set.select_related(
|
||||
"content_type", "group"
|
||||
).all()
|
||||
],
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Iterable
|
||||
|
||||
from baserow.config.celery import app
|
||||
|
||||
|
||||
|
@ -92,3 +94,33 @@ def broadcast_to_group(self, group_id, payload, ignore_web_socket_id=None):
|
|||
return
|
||||
|
||||
broadcast_to_users(user_ids, payload, ignore_web_socket_id)
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def broadcast_to_groups(
|
||||
self, group_ids: Iterable[int], payload: dict, ignore_web_socket_id: str = None
|
||||
):
|
||||
"""
|
||||
Broadcasts a JSON payload to all users that are in the provided groups.
|
||||
|
||||
:param group_ids: Ids of groups to broadcast to.
|
||||
:param payload: A dictionary object containing the payload that must be
|
||||
broadcasted.
|
||||
:param ignore_web_socket_id: The web socket id to which the message must not be
|
||||
sent. This is normally the web socket id that has originally made the change
|
||||
request.
|
||||
"""
|
||||
|
||||
from baserow.core.models import GroupUser
|
||||
|
||||
user_ids = list(
|
||||
GroupUser.objects.filter(group_id__in=group_ids)
|
||||
.distinct("user_id")
|
||||
.order_by("user_id")
|
||||
.values_list("user_id", flat=True)
|
||||
)
|
||||
|
||||
if len(user_ids) == 0:
|
||||
return
|
||||
|
||||
broadcast_to_users(user_ids, payload, ignore_web_socket_id)
|
||||
|
|
|
@ -5,6 +5,7 @@ from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NO
|
|||
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import Group, GroupUser
|
||||
from baserow.test_utils.helpers import is_dict_subset
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -12,8 +13,12 @@ def test_list_groups(api_client, data_fixture):
|
|||
user, token = data_fixture.create_user_and_token(
|
||||
email="test@test.nl", password="password", first_name="Test1"
|
||||
)
|
||||
user_group_2 = data_fixture.create_user_group(user=user, order=2)
|
||||
user_group_1 = data_fixture.create_user_group(user=user, order=1)
|
||||
user_group_2 = data_fixture.create_user_group(
|
||||
user=user, order=2, permissions="ADMIN"
|
||||
)
|
||||
user_group_1 = data_fixture.create_user_group(
|
||||
user=user, order=1, permissions="MEMBER"
|
||||
)
|
||||
data_fixture.create_group()
|
||||
|
||||
response = api_client.get(
|
||||
|
@ -24,8 +29,107 @@ def test_list_groups(api_client, data_fixture):
|
|||
assert len(response_json) == 2
|
||||
assert response_json[0]["id"] == user_group_1.group.id
|
||||
assert response_json[0]["order"] == 1
|
||||
assert response_json[0]["name"] == user_group_1.group.name
|
||||
assert response_json[0]["permissions"] == "MEMBER"
|
||||
assert response_json[1]["id"] == user_group_2.group.id
|
||||
assert response_json[1]["order"] == 2
|
||||
assert response_json[1]["name"] == user_group_2.group.name
|
||||
assert response_json[1]["permissions"] == "ADMIN"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_groups_with_users(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email="test@test.nl", password="password", first_name="Test1"
|
||||
)
|
||||
user_group_1 = data_fixture.create_user_group(
|
||||
user=user, order=1, permissions="MEMBER"
|
||||
)
|
||||
user_group_1_2 = data_fixture.create_user_group(
|
||||
order=1, permissions="ADMIN", group=user_group_1.group
|
||||
)
|
||||
user_group_1_3 = data_fixture.create_user_group(
|
||||
order=1, permissions="MEMBER", group=user_group_1.group
|
||||
)
|
||||
|
||||
user_group_2 = data_fixture.create_user_group(
|
||||
user=user, order=2, permissions="ADMIN"
|
||||
)
|
||||
user_group_2_2 = data_fixture.create_user_group(
|
||||
order=2, permissions="MEMBER", group=user_group_2.group
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:groups:list"), **{"HTTP_AUTHORIZATION": f"JWT {token}"}
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
|
||||
expected_result = [
|
||||
{
|
||||
"id": user_group_1.group.id,
|
||||
"name": user_group_1.group.name,
|
||||
"order": 1,
|
||||
"permissions": "MEMBER",
|
||||
"users": [
|
||||
{
|
||||
"email": user_group_1.user.email,
|
||||
"group": user_group_1.group.id,
|
||||
"id": user_group_1.id,
|
||||
"name": user_group_1.user.first_name,
|
||||
"permissions": "MEMBER",
|
||||
"to_be_deleted": False,
|
||||
"user_id": user_group_1.user.id,
|
||||
},
|
||||
{
|
||||
"email": user_group_1_2.user.email,
|
||||
"group": user_group_1_2.group.id,
|
||||
"id": user_group_1_2.id,
|
||||
"name": user_group_1_2.user.first_name,
|
||||
"permissions": "ADMIN",
|
||||
"to_be_deleted": False,
|
||||
"user_id": user_group_1_2.user.id,
|
||||
},
|
||||
{
|
||||
"email": user_group_1_3.user.email,
|
||||
"group": user_group_1_3.group.id,
|
||||
"id": user_group_1_3.id,
|
||||
"name": user_group_1_3.user.first_name,
|
||||
"permissions": "MEMBER",
|
||||
"to_be_deleted": False,
|
||||
"user_id": user_group_1_3.user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": user_group_2.group.id,
|
||||
"name": user_group_2.group.name,
|
||||
"order": 2,
|
||||
"permissions": "ADMIN",
|
||||
"users": [
|
||||
{
|
||||
"email": user_group_2.user.email,
|
||||
"group": user_group_2.group.id,
|
||||
"id": user_group_2.id,
|
||||
"name": user_group_2.user.first_name,
|
||||
"permissions": "ADMIN",
|
||||
"to_be_deleted": False,
|
||||
"user_id": user_group_2.user.id,
|
||||
},
|
||||
{
|
||||
"email": user_group_2_2.user.email,
|
||||
"group": user_group_2_2.group.id,
|
||||
"id": user_group_2_2.id,
|
||||
"name": user_group_2_2.user.first_name,
|
||||
"permissions": "MEMBER",
|
||||
"to_be_deleted": False,
|
||||
"user_id": user_group_2_2.user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
assert is_dict_subset(expected_result, response_json)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -10,6 +12,78 @@ from baserow.core.models import (
|
|||
GROUP_USER_PERMISSION_MEMBER,
|
||||
)
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
from baserow.core.user.handler import UserHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_groups")
|
||||
def test_user_updated_name(mock_broadcast_to_groups, data_fixture):
|
||||
user = data_fixture.create_user(first_name="Albert")
|
||||
group_user = CoreHandler().create_group(user=user, name="Test")
|
||||
group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
|
||||
|
||||
UserHandler().update_user(user, first_name="Jack")
|
||||
|
||||
mock_broadcast_to_groups.delay.assert_called_once()
|
||||
args = mock_broadcast_to_groups.delay.call_args
|
||||
assert args[0][0] == [group_user.group.id, group_user_2.group.id]
|
||||
assert args[0][1]["type"] == "user_updated"
|
||||
assert args[0][1]["user"]["id"] == user.id
|
||||
assert args[0][1]["user"]["first_name"] == "Jack"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_groups")
|
||||
def test_schedule_user_deletion(mock_broadcast_to_groups, data_fixture):
|
||||
user = data_fixture.create_user(first_name="Albert", password="albert")
|
||||
group_user = CoreHandler().create_group(user=user, name="Test")
|
||||
group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
|
||||
|
||||
UserHandler().schedule_user_deletion(user, password="albert")
|
||||
|
||||
mock_broadcast_to_groups.delay.assert_called_once()
|
||||
args = mock_broadcast_to_groups.delay.call_args
|
||||
assert args[0][0] == [group_user.group.id, group_user_2.group.id]
|
||||
assert args[0][1]["type"] == "user_deleted"
|
||||
assert args[0][1]["user"]["id"] == user.id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_groups")
|
||||
def test_cancel_user_deletion(mock_broadcast_to_groups, data_fixture):
|
||||
user = data_fixture.create_user(first_name="Albert", password="albert")
|
||||
user.profile.to_be_deleted = True
|
||||
user.save()
|
||||
group_user = CoreHandler().create_group(user=user, name="Test")
|
||||
group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
|
||||
|
||||
UserHandler().cancel_user_deletion(user)
|
||||
|
||||
mock_broadcast_to_groups.delay.assert_called_once()
|
||||
args = mock_broadcast_to_groups.delay.call_args
|
||||
assert args[0][0] == [group_user.group.id, group_user_2.group.id]
|
||||
assert args[0][1]["type"] == "user_restored"
|
||||
assert args[0][1]["user"]["id"] == user.id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_groups")
|
||||
def test_user_permanently_deleted(mock_broadcast_to_groups, data_fixture):
|
||||
user = data_fixture.create_user(first_name="Albert", password="albert")
|
||||
user.profile.to_be_deleted = True
|
||||
user.profile.save()
|
||||
user.last_login = timezone.now() - timedelta(weeks=100)
|
||||
user.save()
|
||||
group_user = CoreHandler().create_group(user=user, name="Test")
|
||||
group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
|
||||
|
||||
UserHandler().delete_expired_users(grace_delay=timedelta(days=1))
|
||||
|
||||
mock_broadcast_to_groups.delay.assert_called_once()
|
||||
args = mock_broadcast_to_groups.delay.call_args
|
||||
assert args[0][0] == [group_user.group.id, group_user_2.group.id]
|
||||
assert args[0][1]["type"] == "user_permanently_deleted"
|
||||
assert args[0][1]["user_id"] == user.id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
|
@ -106,8 +180,31 @@ def test_group_deleted(mock_broadcast_to_users, data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_users")
|
||||
def test_group_user_updated(mock_broadcast_to_users, data_fixture):
|
||||
@patch("baserow.ws.signals.broadcast_to_group")
|
||||
def test_group_user_added(mock_broadcast_to_group, data_fixture):
|
||||
user_1 = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group()
|
||||
group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
|
||||
group_invitation = data_fixture.create_group_invitation(
|
||||
email=user_2.email, permissions="MEMBER", group=group
|
||||
)
|
||||
|
||||
group_user_2 = CoreHandler().accept_group_invitation(user_2, group_invitation)
|
||||
|
||||
mock_broadcast_to_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_group.delay.call_args
|
||||
assert args[0][0] == group.id
|
||||
assert args[0][1]["type"] == "group_user_added"
|
||||
assert args[0][1]["id"] == group_user_2.id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_2.user_id
|
||||
assert args[0][1]["group_user"]["permissions"] == "MEMBER"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_group")
|
||||
def test_group_user_updated(mock_broadcast_to_group, data_fixture):
|
||||
user_1 = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group()
|
||||
|
@ -117,31 +214,76 @@ def test_group_user_updated(mock_broadcast_to_users, data_fixture):
|
|||
user=user_2, group_user=group_user_1, permissions="MEMBER"
|
||||
)
|
||||
|
||||
mock_broadcast_to_users.delay.assert_called_once()
|
||||
args = mock_broadcast_to_users.delay.call_args
|
||||
assert args[0][0] == [user_1.id]
|
||||
assert args[0][1]["type"] == "group_updated"
|
||||
assert args[0][1]["group"]["id"] == group.id
|
||||
assert args[0][1]["group"]["name"] == group.name
|
||||
assert args[0][1]["group"]["permissions"] == "MEMBER"
|
||||
mock_broadcast_to_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_group.delay.call_args
|
||||
assert args[0][0] == group.id
|
||||
assert args[0][1]["type"] == "group_user_updated"
|
||||
assert args[0][1]["id"] == group_user_1.id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
|
||||
assert args[0][1]["group_user"]["permissions"] == "MEMBER"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_group")
|
||||
@patch("baserow.ws.signals.broadcast_to_users")
|
||||
def test_group_user_deleted(mock_broadcast_to_users, data_fixture):
|
||||
def test_group_user_deleted(
|
||||
mock_broadcast_to_users, mock_broadcast_to_group, data_fixture
|
||||
):
|
||||
user_1 = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group()
|
||||
group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
|
||||
group_user_id = group_user_1.id
|
||||
data_fixture.create_user_group(user=user_2, group=group)
|
||||
CoreHandler().delete_group_user(user=user_2, group_user=group_user_1)
|
||||
|
||||
mock_broadcast_to_users.delay.assert_called_once()
|
||||
args = mock_broadcast_to_users.delay.call_args
|
||||
assert args[0][0] == [user_1.id]
|
||||
assert args[0][1]["type"] == "group_deleted"
|
||||
assert args[0][1]["type"] == "group_user_deleted"
|
||||
assert args[0][1]["id"] == group_user_id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
|
||||
|
||||
mock_broadcast_to_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_group.delay.call_args
|
||||
assert args[0][0] == group.id
|
||||
assert args[0][1]["type"] == "group_user_deleted"
|
||||
assert args[0][1]["id"] == group_user_id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.signals.broadcast_to_group")
|
||||
@patch("baserow.ws.signals.broadcast_to_users")
|
||||
def test_user_leaves_group(
|
||||
mock_broadcast_to_users, mock_broadcast_to_group, data_fixture
|
||||
):
|
||||
user_1 = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group()
|
||||
group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
|
||||
group_user_id = group_user_1.id
|
||||
data_fixture.create_user_group(user=user_2, group=group)
|
||||
CoreHandler().leave_group(user_1, group)
|
||||
|
||||
mock_broadcast_to_users.delay.assert_called_once()
|
||||
args = mock_broadcast_to_users.delay.call_args
|
||||
assert args[0][0] == [user_1.id]
|
||||
assert args[0][1]["type"] == "group_user_deleted"
|
||||
assert args[0][1]["id"] == group_user_id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
|
||||
|
||||
mock_broadcast_to_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_group.delay.call_args
|
||||
assert args[0][0] == group.id
|
||||
assert args[0][1]["type"] == "group_user_deleted"
|
||||
assert args[0][1]["id"] == group_user_id
|
||||
assert args[0][1]["group_id"] == group.id
|
||||
assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
|
|
|
@ -7,6 +7,7 @@ from baserow.config.asgi import application
|
|||
from baserow.ws.tasks import (
|
||||
broadcast_to_channel_group,
|
||||
broadcast_to_group,
|
||||
broadcast_to_groups,
|
||||
broadcast_to_users,
|
||||
)
|
||||
|
||||
|
@ -234,3 +235,89 @@ async def test_broadcast_to_group(data_fixture):
|
|||
await communicator_1.disconnect()
|
||||
await communicator_2.disconnect()
|
||||
await communicator_3.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.run(order=6)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
async def test_broadcast_to_groups(data_fixture):
|
||||
user_1, token_1 = data_fixture.create_user_and_token()
|
||||
user_2, token_2 = data_fixture.create_user_and_token()
|
||||
user_3, token_3 = data_fixture.create_user_and_token()
|
||||
user_4, token_4 = data_fixture.create_user_and_token()
|
||||
group_1 = data_fixture.create_group(users=[user_1, user_2, user_4])
|
||||
group_2 = data_fixture.create_group(users=[user_2, user_3])
|
||||
|
||||
communicator_1 = WebsocketCommunicator(
|
||||
application,
|
||||
f"ws/core/?jwt_token={token_1}",
|
||||
headers=[(b"origin", b"http://localhost")],
|
||||
)
|
||||
await communicator_1.connect()
|
||||
response_1 = await communicator_1.receive_json_from()
|
||||
web_socket_id_1 = response_1["web_socket_id"]
|
||||
|
||||
communicator_2 = WebsocketCommunicator(
|
||||
application,
|
||||
f"ws/core/?jwt_token={token_2}",
|
||||
headers=[(b"origin", b"http://localhost")],
|
||||
)
|
||||
await communicator_2.connect()
|
||||
response_2 = await communicator_2.receive_json_from()
|
||||
web_socket_id_2 = response_2["web_socket_id"]
|
||||
|
||||
communicator_3 = WebsocketCommunicator(
|
||||
application,
|
||||
f"ws/core/?jwt_token={token_3}",
|
||||
headers=[(b"origin", b"http://localhost")],
|
||||
)
|
||||
await communicator_3.connect()
|
||||
await communicator_3.receive_json_from()
|
||||
|
||||
await database_sync_to_async(broadcast_to_groups)([group_1.id], {"message": "test"})
|
||||
response_1 = await communicator_1.receive_json_from(0.1)
|
||||
response_2 = await communicator_2.receive_json_from(0.1)
|
||||
await communicator_3.receive_nothing(0.1)
|
||||
|
||||
assert response_1["message"] == "test"
|
||||
assert response_2["message"] == "test"
|
||||
|
||||
await database_sync_to_async(broadcast_to_groups)(
|
||||
[group_1.id], {"message": "test2"}, ignore_web_socket_id=web_socket_id_1
|
||||
)
|
||||
|
||||
await communicator_1.receive_nothing(0.1)
|
||||
response_2 = await communicator_2.receive_json_from(0.1)
|
||||
await communicator_3.receive_nothing(0.1)
|
||||
|
||||
assert response_2["message"] == "test2"
|
||||
|
||||
communicator_4 = WebsocketCommunicator(
|
||||
application,
|
||||
f"ws/core/?jwt_token={token_4}",
|
||||
headers=[(b"origin", b"http://localhost")],
|
||||
)
|
||||
await communicator_4.connect()
|
||||
response_4 = await communicator_4.receive_json_from()
|
||||
web_socket_id_4 = response_4["web_socket_id"]
|
||||
|
||||
await database_sync_to_async(broadcast_to_groups)(
|
||||
[group_1.id, group_2.id],
|
||||
{"message": "test3"},
|
||||
ignore_web_socket_id=web_socket_id_4,
|
||||
)
|
||||
|
||||
await communicator_1.receive_json_from(0.1)
|
||||
await communicator_2.receive_json_from(0.1)
|
||||
await communicator_3.receive_json_from(0.1)
|
||||
await communicator_4.receive_nothing(0.1)
|
||||
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
assert communicator_2.output_queue.qsize() == 0
|
||||
assert communicator_3.output_queue.qsize() == 0
|
||||
assert communicator_4.output_queue.qsize() == 0
|
||||
|
||||
await communicator_1.disconnect()
|
||||
await communicator_2.disconnect()
|
||||
await communicator_3.disconnect()
|
||||
await communicator_4.disconnect()
|
||||
|
|
|
@ -19,6 +19,8 @@ For example:
|
|||
* Force browser language when viewing a public view. [#834](https://gitlab.com/bramw/baserow/-/issues/834)
|
||||
* Search automatically after 400ms when chosing a related field via the modal. [#1091](https://gitlab.com/bramw/baserow/-/issues/1091)
|
||||
* Add cancel button to field update context [#1020](https://gitlab.com/bramw/baserow/-/issues/1020)
|
||||
* New signals `user_updated`, `user_deleted`, `user_restored`, `user_permanently_deleted` were added to track user changes.
|
||||
* `list_groups` endpoint now also returns the list of all group users for each group.
|
||||
|
||||
### Bug Fixes
|
||||
* Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113)
|
||||
|
|
|
@ -123,6 +123,10 @@ are subscribed to the page.
|
|||
* `page_add`
|
||||
* `page_discard`
|
||||
* `before_group_deleted`
|
||||
* `user_updated`
|
||||
* `user_deleted`
|
||||
* `user_restored`
|
||||
* `user_permanently_deleted`
|
||||
* `group_created`
|
||||
* `group_updated`
|
||||
* `group_deleted`
|
||||
|
|
|
@ -196,6 +196,11 @@ export default {
|
|||
|
||||
try {
|
||||
await GroupService(this.$client).updateUser(user.id, { permissions })
|
||||
await this.$store.dispatch('group/forceUpdateGroupUser', {
|
||||
groupId: this.group.id,
|
||||
id: user.id,
|
||||
values: { permissions },
|
||||
})
|
||||
} catch (error) {
|
||||
user.permissions = oldPermissions
|
||||
notifyIf(error, 'group')
|
||||
|
@ -208,7 +213,13 @@ export default {
|
|||
try {
|
||||
await GroupService(this.$client).deleteUser(user.id)
|
||||
const index = this.users.findIndex((u) => u.id === user.id)
|
||||
const userId = this.users[index].user_id
|
||||
this.users.splice(index, 1)
|
||||
await this.$store.dispatch('group/forceDeleteGroupUser', {
|
||||
groupId: this.group.id,
|
||||
id: user.id,
|
||||
values: { user_id: userId },
|
||||
})
|
||||
} catch (error) {
|
||||
user._.loading = false
|
||||
notifyIf(error, 'group')
|
||||
|
|
|
@ -199,6 +199,39 @@ export class RealTimeHandler {
|
|||
store.dispatch('auth/forceUpdateUserData', data.user_data)
|
||||
})
|
||||
|
||||
this.registerEvent('user_updated', ({ store }, data) => {
|
||||
store.dispatch('group/forceUpdateGroupUserAttributes', {
|
||||
userId: data.user.id,
|
||||
values: {
|
||||
name: data.user.first_name,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('user_deleted', ({ store }, data) => {
|
||||
store.dispatch('group/forceUpdateGroupUserAttributes', {
|
||||
userId: data.user.id,
|
||||
values: {
|
||||
to_be_deleted: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('user_restored', ({ store }, data) => {
|
||||
store.dispatch('group/forceUpdateGroupUserAttributes', {
|
||||
userId: data.user.id,
|
||||
values: {
|
||||
to_be_deleted: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('user_permanently_deleted', ({ store }, data) => {
|
||||
store.dispatch('group/forceDeleteUser', {
|
||||
userId: data.user_id,
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('group_created', ({ store }, data) => {
|
||||
store.dispatch('group/forceCreate', data.group)
|
||||
})
|
||||
|
@ -226,6 +259,29 @@ export class RealTimeHandler {
|
|||
store.dispatch('group/forceOrder', data.group_ids)
|
||||
})
|
||||
|
||||
this.registerEvent('group_user_added', ({ store }, data) => {
|
||||
store.dispatch('group/forceAddGroupUser', {
|
||||
groupId: data.group_id,
|
||||
values: data.group_user,
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('group_user_updated', ({ store }, data) => {
|
||||
store.dispatch('group/forceUpdateGroupUser', {
|
||||
id: data.id,
|
||||
groupId: data.group_id,
|
||||
values: data.group_user,
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('group_user_deleted', ({ store }, data) => {
|
||||
store.dispatch('group/forceDeleteGroupUser', {
|
||||
id: data.id,
|
||||
groupId: data.group_id,
|
||||
values: data.group_user,
|
||||
})
|
||||
})
|
||||
|
||||
this.registerEvent('application_created', ({ store }, data) => {
|
||||
store.dispatch('application/forceCreate', data.application)
|
||||
})
|
||||
|
|
|
@ -160,9 +160,21 @@ export const actions = {
|
|||
/**
|
||||
* Updates the account information is the authenticated user.
|
||||
*/
|
||||
async update({ commit }, values) {
|
||||
async update({ getters, commit, dispatch }, values) {
|
||||
const { data } = await AuthService(this.$client).update(values)
|
||||
commit('UPDATE_USER_DATA', { user: data })
|
||||
dispatch(
|
||||
'group/forceUpdateGroupUserAttributes',
|
||||
{
|
||||
userId: getters.getUserId,
|
||||
values: {
|
||||
name: data.first_name,
|
||||
},
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
}
|
||||
)
|
||||
return data
|
||||
},
|
||||
forceUpdateUserData({ commit }, data) {
|
||||
|
|
|
@ -69,6 +69,38 @@ export const mutations = {
|
|||
})
|
||||
state.selected = {}
|
||||
},
|
||||
ADD_GROUP_USER(state, { groupId, values }) {
|
||||
const groupIndex = state.items.findIndex((item) => item.id === groupId)
|
||||
if (groupIndex !== -1) {
|
||||
state.items[groupIndex].users.push(values)
|
||||
}
|
||||
},
|
||||
UPDATE_GROUP_USER(state, { groupId, id, values }) {
|
||||
const groupIndex = state.items.findIndex((item) => item.id === groupId)
|
||||
if (groupIndex === -1) {
|
||||
return
|
||||
}
|
||||
const usersIndex = state.items[groupIndex].users.findIndex(
|
||||
(item) => item.id === id
|
||||
)
|
||||
if (usersIndex === -1) {
|
||||
return
|
||||
}
|
||||
Object.assign(
|
||||
state.items[groupIndex].users[usersIndex],
|
||||
state.items[groupIndex].users[usersIndex],
|
||||
values
|
||||
)
|
||||
},
|
||||
DELETE_GROUP_USER(state, { groupId, id }) {
|
||||
const groupIndex = state.items.findIndex((item) => item.id === groupId)
|
||||
if (groupIndex === -1) {
|
||||
return
|
||||
}
|
||||
state.items[groupIndex].users = state.items[groupIndex].users.filter(
|
||||
(item) => item.id !== id
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
|
@ -240,6 +272,77 @@ export const actions = {
|
|||
})
|
||||
return dispatch('application/clearAll', group, { root: true })
|
||||
},
|
||||
/**
|
||||
* Forcefully adds a group user in the list of group's users.
|
||||
*/
|
||||
forceAddGroupUser({ commit }, { groupId, values }) {
|
||||
commit('ADD_GROUP_USER', { groupId, values })
|
||||
},
|
||||
/**
|
||||
* Forcefully updates a group user in the list of group's users
|
||||
* and updates group's permissions attr if the group user is the
|
||||
* same as the current user.
|
||||
*/
|
||||
forceUpdateGroupUser({ commit, rootGetters }, { groupId, id, values }) {
|
||||
commit('UPDATE_GROUP_USER', { groupId, id, values })
|
||||
const userId = rootGetters['auth/getUserId']
|
||||
if (values.user_id === userId) {
|
||||
commit('UPDATE_ITEM', {
|
||||
id: groupId,
|
||||
values: { permissions: values.permissions },
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Forcefully updates user's properties based on userId across all group users
|
||||
* of all groups. Can be used e.g. to change the user's name across all group
|
||||
* users that represent the same user in the system.
|
||||
*/
|
||||
forceUpdateGroupUserAttributes({ commit, rootGetters }, { userId, values }) {
|
||||
const groups = rootGetters['group/getAll']
|
||||
for (const group of groups) {
|
||||
const usersIndex = group.users.findIndex(
|
||||
(item) => item.user_id === userId
|
||||
)
|
||||
if (usersIndex !== -1) {
|
||||
commit('UPDATE_GROUP_USER', {
|
||||
groupId: group.id,
|
||||
id: group.users[usersIndex].id,
|
||||
values,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Forcefully deletes a group user in the list of group's users. If the
|
||||
* group user is the current user, the whole group is removed.
|
||||
*/
|
||||
forceDeleteGroupUser({ commit, rootGetters }, { groupId, id, values }) {
|
||||
const userId = rootGetters['auth/getUserId']
|
||||
if (values.user_id === userId) {
|
||||
commit('DELETE_ITEM', { id: groupId })
|
||||
} else {
|
||||
commit('DELETE_GROUP_USER', { groupId, id })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Forcefully deletes a user by deleting various user group instances
|
||||
* in all groups where the user is present.
|
||||
*/
|
||||
forceDeleteUser({ commit, rootGetters }, { userId }) {
|
||||
const groups = rootGetters['group/getAll']
|
||||
for (const group of groups) {
|
||||
const usersIndex = group.users.findIndex(
|
||||
(item) => item.user_id === userId
|
||||
)
|
||||
if (usersIndex !== -1) {
|
||||
commit('DELETE_GROUP_USER', {
|
||||
groupId: group.id,
|
||||
id: group.users[usersIndex].id,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
|
|
456
web-frontend/test/unit/core/store/group.spec.js
Normal file
456
web-frontend/test/unit/core/store/group.spec.js
Normal file
|
@ -0,0 +1,456 @@
|
|||
import groupStore from '@baserow/modules/core/store/group'
|
||||
import { TestApp } from '@baserow/test/helpers/testApp'
|
||||
|
||||
describe('Group store', () => {
|
||||
let testApp = null
|
||||
let store = null
|
||||
|
||||
beforeEach(() => {
|
||||
testApp = new TestApp()
|
||||
store = testApp.store
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
testApp.afterEach()
|
||||
})
|
||||
|
||||
test('forceAddGroupUser adds user to a group', async () => {
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceAddGroupUser', {
|
||||
groupId: 1,
|
||||
values: {
|
||||
id: 74,
|
||||
user_id: 257,
|
||||
group: 1,
|
||||
name: 'Adam',
|
||||
email: 'adam@example.com',
|
||||
permissions: 'MEMBER',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users.length).toBe(2)
|
||||
expect(group.users[1].user_id).toBe(257)
|
||||
expect(group.users[1].permissions).toBe('MEMBER')
|
||||
})
|
||||
|
||||
test('forceUpdateGroupUser updates a user from the group', async () => {
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceUpdateGroupUser', {
|
||||
groupId: 1,
|
||||
id: 73,
|
||||
values: {
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'Petr',
|
||||
email: 'petr@example.com',
|
||||
permissions: 'MEMBER',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users.length).toBe(1)
|
||||
expect(group.users[0].name).toBe('Petr')
|
||||
expect(group.users[0].permissions).toBe('MEMBER')
|
||||
})
|
||||
|
||||
test(`forceUpdateGroupUser updates a current group permissions
|
||||
when the current user is updated`, async () => {
|
||||
await store.dispatch('auth/forceSetUserData', {
|
||||
user: {
|
||||
id: 256,
|
||||
},
|
||||
token:
|
||||
`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvaG5AZXhhb` +
|
||||
`XBsZS5jb20iLCJpYXQiOjE2NjAyOTEwODYsImV4cCI6MTY2MDI5NDY4NiwianRpIjo` +
|
||||
`iNDZmNzUwZWUtMTJhMS00N2UzLWJiNzQtMDIwYWM4Njg3YWMzIiwidXNlcl9pZCI6M` +
|
||||
`iwidXNlcl9wcm9maWxlX2lkIjpbMl0sIm9yaWdfaWF0IjoxNjYwMjkxMDg2fQ.RQ-M` +
|
||||
`NQdDR9zTi8CbbQkRrwNsyDa5CldQI83Uid1l9So`,
|
||||
})
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceUpdateGroupUser', {
|
||||
groupId: 1,
|
||||
id: 73,
|
||||
values: {
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'Petr',
|
||||
email: 'petr@example.com',
|
||||
permissions: 'MEMBER',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users.length).toBe(1)
|
||||
expect(group.users[0].name).toBe('Petr')
|
||||
expect(group.users[0].permissions).toBe('MEMBER')
|
||||
|
||||
expect(group.permissions).toBe('MEMBER')
|
||||
})
|
||||
|
||||
test('forceUpdateGroupUserAttributes updates a user across all groups', async () => {
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group 2',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 2136,
|
||||
user_id: 456,
|
||||
group: 2,
|
||||
name: 'Peter',
|
||||
email: 'peter@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
{
|
||||
id: 173,
|
||||
user_id: 256,
|
||||
group: 2,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 3',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 2132,
|
||||
user_id: 456,
|
||||
group: 3,
|
||||
name: 'Peter',
|
||||
email: 'peter@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceUpdateGroupUserAttributes', {
|
||||
userId: 256,
|
||||
values: {
|
||||
name: 'John renamed',
|
||||
to_be_deleted: true,
|
||||
},
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users[0].name).toBe('John renamed')
|
||||
expect(group.users[0].to_be_deleted).toBe(true)
|
||||
|
||||
const group2 = store.getters['test/get'](2)
|
||||
expect(group2.users[0].name).toBe('Peter')
|
||||
expect(group2.users[0].to_be_deleted).toBe(false)
|
||||
expect(group2.users[1].name).toBe('John renamed')
|
||||
expect(group2.users[1].to_be_deleted).toBe(true)
|
||||
|
||||
const group3 = store.getters['test/get'](3)
|
||||
expect(group3.users[0].name).toBe('Peter')
|
||||
expect(group3.users[0].to_be_deleted).toBe(false)
|
||||
})
|
||||
|
||||
test('forceDeleteGroupUser removes a user from the group', async () => {
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceDeleteGroupUser', {
|
||||
groupId: 1,
|
||||
id: 73,
|
||||
values: {
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users.length).toBe(0)
|
||||
})
|
||||
|
||||
test(`forceDeleteGroupUser removes the whole group if the
|
||||
current user is being removed`, async () => {
|
||||
await store.dispatch('auth/forceSetUserData', {
|
||||
user: {
|
||||
id: 256,
|
||||
},
|
||||
token:
|
||||
`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvaG5AZXhhb` +
|
||||
`XBsZS5jb20iLCJpYXQiOjE2NjAyOTEwODYsImV4cCI6MTY2MDI5NDY4NiwianRpIjo` +
|
||||
`iNDZmNzUwZWUtMTJhMS00N2UzLWJiNzQtMDIwYWM4Njg3YWMzIiwidXNlcl9pZCI6M` +
|
||||
`iwidXNlcl9wcm9maWxlX2lkIjpbMl0sIm9yaWdfaWF0IjoxNjYwMjkxMDg2fQ.RQ-M` +
|
||||
`NQdDR9zTi8CbbQkRrwNsyDa5CldQI83Uid1l9So`,
|
||||
})
|
||||
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceDeleteGroupUser', {
|
||||
groupId: 1,
|
||||
id: 73,
|
||||
values: {
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
})
|
||||
|
||||
const groups = store.getters['test/getAll']
|
||||
expect(groups.length).toBe(0)
|
||||
})
|
||||
|
||||
test('forceDeleteUser deletes all group users across all groups', async () => {
|
||||
const state = Object.assign(groupStore.state(), {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 1',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 73,
|
||||
user_id: 256,
|
||||
group: 1,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group 2',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 2136,
|
||||
user_id: 456,
|
||||
group: 2,
|
||||
name: 'Peter',
|
||||
email: 'peter@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
{
|
||||
id: 173,
|
||||
user_id: 256,
|
||||
group: 2,
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 3',
|
||||
order: 1,
|
||||
permissions: 'ADMIN',
|
||||
users: [
|
||||
{
|
||||
id: 2132,
|
||||
user_id: 456,
|
||||
group: 3,
|
||||
name: 'Peter',
|
||||
email: 'peter@example.com',
|
||||
permissions: 'ADMIN',
|
||||
to_be_deleted: false,
|
||||
created_on: '2022-08-10T14:20:05.629890Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
groupStore.state = () => state
|
||||
store.registerModule('test', groupStore)
|
||||
|
||||
await store.dispatch('test/forceDeleteUser', {
|
||||
userId: 256,
|
||||
})
|
||||
|
||||
const group = store.getters['test/get'](1)
|
||||
expect(group.users.length).toBe(0)
|
||||
|
||||
const group2 = store.getters['test/get'](2)
|
||||
expect(group2.users[0].name).toBe('Peter')
|
||||
expect(group2.users[0].to_be_deleted).toBe(false)
|
||||
expect(group2.users.length).toBe(1)
|
||||
|
||||
const group3 = store.getters['test/get'](3)
|
||||
expect(group3.users.length).toBe(1)
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue