1
0
Fork 0
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:
Petr Stribny 2022-08-31 15:49:01 +00:00
parent 9dfb3c2330
commit 95c0a54c12
23 changed files with 1254 additions and 97 deletions

2
.gitignore vendored
View file

@ -127,3 +127,5 @@ junit.xml
!plugin-boilerplate/{{ cookiecutter.project_slug }}/.env
field-diagrams/
*.http

View file

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

View file

@ -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",
},
}
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)
})
})