1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 00:59:06 +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 !plugin-boilerplate/{{ cookiecutter.project_slug }}/.env
field-diagrams/ field-diagrams/
*.http

View file

@ -324,7 +324,10 @@ class AcceptGroupInvitationView(APIView):
group_user = CoreHandler().accept_group_invitation( group_user = CoreHandler().accept_group_invitation(
request.user, 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): 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 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): class GroupSerializer(serializers.ModelSerializer):

View file

@ -39,22 +39,36 @@ class GroupUserSerializer(serializers.ModelSerializer):
return object.user.email return object.user.email
class GroupUserGroupSerializer(serializers.ModelSerializer): class GroupUserGroupSerializer(serializers.Serializer):
""" """
This serializers returns all the fields that the GroupSerializer has, but also This serializers includes relevant fields of the Group model, but also
some user specific values related to the group user relation. some GroupUser specific fields related to the group user relation.
Additionally, the list of users are included for each group.
""" """
class Meta: # Group fields
model = GroupUser id = serializers.IntegerField(
fields = ("order", "permissions") 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): # GroupUser fields
from baserow.api.groups.serializers import GroupSerializer order = serializers.IntegerField(
read_only=True, help_text="The requesting user's order within the group users."
data = super().to_representation(instance) )
data.update(GroupSerializer(instance.group).data) permissions = serializers.CharField(
return data read_only=True, help_text="The requesting user's permissions for the group."
)
class UpdateGroupUserSerializer(serializers.ModelSerializer): class UpdateGroupUserSerializer(serializers.ModelSerializer):

View file

@ -23,11 +23,7 @@ from baserow.core.exceptions import (
from baserow.core.handler import CoreHandler from baserow.core.handler import CoreHandler
from baserow.core.models import GroupUser from baserow.core.models import GroupUser
from .serializers import ( from .serializers import GroupUserSerializer, UpdateGroupUserSerializer
GroupUserGroupSerializer,
GroupUserSerializer,
UpdateGroupUserSerializer,
)
class GroupUsersView(APIView): class GroupUsersView(APIView):
@ -95,7 +91,7 @@ class GroupUserView(APIView):
), ),
request=UpdateGroupUserSerializer, request=UpdateGroupUserSerializer,
responses={ responses={
200: GroupUserGroupSerializer, 200: GroupUserSerializer,
400: get_error_schema( 400: get_error_schema(
[ [
"ERROR_USER_NOT_IN_GROUP", "ERROR_USER_NOT_IN_GROUP",
@ -123,7 +119,7 @@ class GroupUserView(APIView):
base_queryset=GroupUser.objects.select_for_update(of=("self",)), base_queryset=GroupUser.objects.select_for_update(of=("self",)),
) )
group_user = CoreHandler().update_group_user(request.user, group_user, **data) 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( @extend_schema(
parameters=[ parameters=[

View file

@ -1,7 +1,6 @@
from django.db import transaction from django.db import transaction
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
from drf_spectacular.plumbing import build_array_type
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
@ -34,11 +33,9 @@ from baserow.core.exceptions import (
UserNotInGroup, UserNotInGroup,
) )
from baserow.core.handler import CoreHandler from baserow.core.handler import CoreHandler
from baserow.core.models import GroupUser
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
from .errors import ERROR_GROUP_USER_IS_LAST_ADMIN from .errors import ERROR_GROUP_USER_IS_LAST_ADMIN
from .schemas import group_user_schema
from .serializers import GroupSerializer, OrderGroupsSerializer from .serializers import GroupSerializer, OrderGroupsSerializer
@ -56,13 +53,15 @@ class GroupsView(APIView):
"are custom for each user. The order is configurable via the " "are custom for each user. The order is configurable via the "
"**order_groups** endpoint." "**order_groups** endpoint."
), ),
responses={200: build_array_type(group_user_schema)}, responses={200: GroupUserGroupSerializer(many=True)},
) )
def get(self, request): def get(self, request):
"""Responds with a list of serialized groups where the user is part of.""" """Responds with a list of serialized groups where the user is part of."""
groups = GroupUser.objects.filter(user=request.user).select_related("group") groupuser_groups = (
serializer = GroupUserGroupSerializer(groups, many=True) CoreHandler().get_groupuser_group_queryset().filter(user=request.user)
)
serializer = GroupUserGroupSerializer(groupuser_groups, many=True)
return Response(serializer.data) return Response(serializer.data)
@extend_schema( @extend_schema(
@ -75,7 +74,7 @@ class GroupsView(APIView):
"created via other endpoints." "created via other endpoints."
), ),
request=GroupSerializer, request=GroupSerializer,
responses={200: group_user_schema}, responses={200: GroupUserGroupSerializer},
) )
@transaction.atomic @transaction.atomic
@validate_body(GroupSerializer) @validate_body(GroupSerializer)
@ -160,7 +159,7 @@ class GroupView(APIView):
), ),
request=GroupSerializer, request=GroupSerializer,
responses={ responses={
200: group_user_schema, 204: None,
400: get_error_schema( 400: get_error_schema(
[ [
"ERROR_USER_NOT_IN_GROUP", "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): class RegisterSerializer(serializers.Serializer):
name = serializers.CharField(min_length=2, max_length=150) name = serializers.CharField(min_length=2, max_length=150)
email = serializers.EmailField( 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.contrib.auth.models import AbstractUser
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db import transaction 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 django.utils import translation
from itsdangerous import URLSafeSerializer from itsdangerous import URLSafeSerializer
@ -153,6 +153,22 @@ class CoreHandler:
return group 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: def create_group(self, user: User, name: str) -> GroupUser:
""" """
Creates a new group for an existing user. Creates a new group for an existing user.
@ -243,7 +259,10 @@ class CoreHandler:
group_user_id = group_user.id group_user_id = group_user.id
group_user.delete() group_user.delete()
group_user_deleted.send( 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): def delete_group_by_id(self, user: AbstractUser, group_id: int):
@ -394,7 +413,10 @@ class CoreHandler:
group_user.delete() group_user.delete()
group_user_deleted.send( 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): def get_group_invitation_signer(self):

View file

@ -6,6 +6,11 @@ before_user_deleted = Signal()
before_group_deleted = Signal() before_group_deleted = Signal()
user_updated = Signal()
user_deleted = Signal()
user_restored = Signal()
user_permanently_deleted = Signal()
group_created = Signal() group_created = Signal()
group_updated = Signal() group_updated = Signal()
group_deleted = Signal() group_deleted = Signal()

View file

@ -19,9 +19,15 @@ from baserow.core.exceptions import (
GroupInvitationEmailMismatch, GroupInvitationEmailMismatch,
) )
from baserow.core.handler import CoreHandler 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.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 baserow.core.trash.handler import TrashHandler
from .emails import ( from .emails import (
@ -189,7 +195,8 @@ class UserHandler:
language: Optional[str] = None, language: Optional[str] = None,
) -> AbstractUser: ) -> 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 user: The user instance to update.
:param first_name: The new user first name. :param first_name: The new user first name.
@ -205,6 +212,8 @@ class UserHandler:
user.profile.language = language user.profile.language = language
user.profile.save() user.profile.save()
user_updated.send(self, performed_by=user, user=user)
return user return user
def get_reset_password_signer(self) -> URLSafeTimedSerializer: def get_reset_password_signer(self) -> URLSafeTimedSerializer:
@ -374,6 +383,8 @@ class UserHandler:
email = AccountDeletionScheduled(user, days_left, to=[user.email]) email = AccountDeletionScheduled(user, days_left, to=[user.email])
email.send() email.send()
user_deleted.send(self, performed_by=user, user=user)
def cancel_user_deletion(self, user: AbstractUser): def cancel_user_deletion(self, user: AbstractUser):
""" """
Cancels a previously scheduled user account deletion. This action send an email 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 = AccountDeletionCanceled(user, to=[user.email])
email.send() email.send()
user_restored.send(self, performed_by=user, user=user)
def delete_expired_users(self, grace_delay: Optional[timedelta] = None): def delete_expired_users(self, grace_delay: Optional[timedelta] = None):
""" """
Executes all previously scheduled user account deletions for which 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 profile__to_be_deleted=True, last_login__lt=limit_date
) )
deleted_user_info = [ group_users = GroupUser.objects.filter(user__in=users_to_delete)
(u.username, u.email, u.profile.language) for u in users_to_delete.all()
] 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 # A group need to be deleted if there was an admin before and there is no
# *active* admin after the users deletion. # *active* admin after the users deletion.
@ -447,7 +465,8 @@ class UserHandler:
TrashHandler.permanently_delete(group) TrashHandler.permanently_delete(group)
users_to_delete.delete() 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): with translation.override(language):
email = AccountDeleted(username, to=[email]) email = AccountDeleted(username, to=[email])
email.send() 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 django.dispatch import receiver
from baserow.api.applications.serializers import get_application_serializer 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 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) @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) @receiver(signals.group_user_added)
def group_user_updated(sender, group_user, user, **kwargs): def group_user_added(sender, group_user, user, **kwargs):
transaction.on_commit( transaction.on_commit(
lambda: broadcast_to_users.delay( lambda: broadcast_to_group.delay(
[group_user.user_id], group_user.group_id,
{ {
"type": "group_updated", "type": "group_user_added",
"id": group_user.id,
"group_id": group_user.group_id, "group_id": group_user.group_id,
"group": GroupUserGroupSerializer(group_user).data, "group_user": GroupUserSerializer(group_user).data,
}, },
getattr(user, "web_socket_id", None), getattr(user, "web_socket_id", None),
) )
) )
@receiver(signals.group_restored) @receiver(signals.group_user_updated)
def group_restored(sender, group_user, user, **kwargs): def group_user_updated(sender, group_user, user, **kwargs):
transaction.on_commit( transaction.on_commit(
lambda: broadcast_to_users.delay( lambda: broadcast_to_group.delay(
[group_user.user_id], group_user.group_id,
{ {
"type": "group_restored", "type": "group_user_updated",
"id": group_user.id,
"group_id": group_user.group_id, "group_id": group_user.group_id,
"group": GroupUserGroupSerializer(group_user).data, "group_user": GroupUserSerializer(group_user).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), getattr(user, "web_socket_id", None),
) )
@ -82,11 +136,47 @@ def group_restored(sender, group_user, user, **kwargs):
@receiver(signals.group_user_deleted) @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( transaction.on_commit(
lambda: broadcast_to_users.delay( lambda: broadcast_to_users.delay(
[group_user.user_id], [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), getattr(user, "web_socket_id", None),
) )
) )

View file

@ -1,3 +1,5 @@
from typing import Iterable
from baserow.config.celery import app from baserow.config.celery import app
@ -92,3 +94,33 @@ def broadcast_to_group(self, group_id, payload, ignore_web_socket_id=None):
return return
broadcast_to_users(user_ids, payload, ignore_web_socket_id) 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.handler import CoreHandler
from baserow.core.models import Group, GroupUser from baserow.core.models import Group, GroupUser
from baserow.test_utils.helpers import is_dict_subset
@pytest.mark.django_db @pytest.mark.django_db
@ -12,8 +13,12 @@ def test_list_groups(api_client, data_fixture):
user, token = data_fixture.create_user_and_token( user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1" email="test@test.nl", password="password", first_name="Test1"
) )
user_group_2 = data_fixture.create_user_group(user=user, order=2) user_group_2 = data_fixture.create_user_group(
user_group_1 = data_fixture.create_user_group(user=user, order=1) user=user, order=2, permissions="ADMIN"
)
user_group_1 = data_fixture.create_user_group(
user=user, order=1, permissions="MEMBER"
)
data_fixture.create_group() data_fixture.create_group()
response = api_client.get( response = api_client.get(
@ -24,8 +29,107 @@ def test_list_groups(api_client, data_fixture):
assert len(response_json) == 2 assert len(response_json) == 2
assert response_json[0]["id"] == user_group_1.group.id assert response_json[0]["id"] == user_group_1.group.id
assert response_json[0]["order"] == 1 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]["id"] == user_group_2.group.id
assert response_json[1]["order"] == 2 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 @pytest.mark.django_db

View file

@ -1,6 +1,8 @@
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from django.db import transaction from django.db import transaction
from django.utils import timezone
import pytest import pytest
@ -10,6 +12,78 @@ from baserow.core.models import (
GROUP_USER_PERMISSION_MEMBER, GROUP_USER_PERMISSION_MEMBER,
) )
from baserow.core.trash.handler import TrashHandler 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) @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) @pytest.mark.django_db(transaction=True)
@patch("baserow.ws.signals.broadcast_to_users") @patch("baserow.ws.signals.broadcast_to_group")
def test_group_user_updated(mock_broadcast_to_users, data_fixture): 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_1 = data_fixture.create_user()
user_2 = data_fixture.create_user() user_2 = data_fixture.create_user()
group = data_fixture.create_group() 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" user=user_2, group_user=group_user_1, permissions="MEMBER"
) )
mock_broadcast_to_users.delay.assert_called_once() mock_broadcast_to_group.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args args = mock_broadcast_to_group.delay.call_args
assert args[0][0] == [user_1.id] assert args[0][0] == group.id
assert args[0][1]["type"] == "group_updated" assert args[0][1]["type"] == "group_user_updated"
assert args[0][1]["group"]["id"] == group.id assert args[0][1]["id"] == group_user_1.id
assert args[0][1]["group"]["name"] == group.name
assert args[0][1]["group"]["permissions"] == "MEMBER"
assert args[0][1]["group_id"] == group.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) @pytest.mark.django_db(transaction=True)
@patch("baserow.ws.signals.broadcast_to_group")
@patch("baserow.ws.signals.broadcast_to_users") @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_1 = data_fixture.create_user()
user_2 = data_fixture.create_user() user_2 = data_fixture.create_user()
group = data_fixture.create_group() group = data_fixture.create_group()
group_user_1 = data_fixture.create_user_group(user=user_1, group=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) data_fixture.create_user_group(user=user_2, group=group)
CoreHandler().delete_group_user(user=user_2, group_user=group_user_1) CoreHandler().delete_group_user(user=user_2, group_user=group_user_1)
mock_broadcast_to_users.delay.assert_called_once() mock_broadcast_to_users.delay.assert_called_once()
args = mock_broadcast_to_users.delay.call_args args = mock_broadcast_to_users.delay.call_args
assert args[0][0] == [user_1.id] 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_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) @pytest.mark.django_db(transaction=True)

View file

@ -7,6 +7,7 @@ from baserow.config.asgi import application
from baserow.ws.tasks import ( from baserow.ws.tasks import (
broadcast_to_channel_group, broadcast_to_channel_group,
broadcast_to_group, broadcast_to_group,
broadcast_to_groups,
broadcast_to_users, broadcast_to_users,
) )
@ -234,3 +235,89 @@ async def test_broadcast_to_group(data_fixture):
await communicator_1.disconnect() await communicator_1.disconnect()
await communicator_2.disconnect() await communicator_2.disconnect()
await communicator_3.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) * 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) * 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) * 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 ### Bug Fixes
* Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113) * 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_add`
* `page_discard` * `page_discard`
* `before_group_deleted` * `before_group_deleted`
* `user_updated`
* `user_deleted`
* `user_restored`
* `user_permanently_deleted`
* `group_created` * `group_created`
* `group_updated` * `group_updated`
* `group_deleted` * `group_deleted`

View file

@ -196,6 +196,11 @@ export default {
try { try {
await GroupService(this.$client).updateUser(user.id, { permissions }) 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) { } catch (error) {
user.permissions = oldPermissions user.permissions = oldPermissions
notifyIf(error, 'group') notifyIf(error, 'group')
@ -208,7 +213,13 @@ export default {
try { try {
await GroupService(this.$client).deleteUser(user.id) await GroupService(this.$client).deleteUser(user.id)
const index = this.users.findIndex((u) => u.id === user.id) const index = this.users.findIndex((u) => u.id === user.id)
const userId = this.users[index].user_id
this.users.splice(index, 1) this.users.splice(index, 1)
await this.$store.dispatch('group/forceDeleteGroupUser', {
groupId: this.group.id,
id: user.id,
values: { user_id: userId },
})
} catch (error) { } catch (error) {
user._.loading = false user._.loading = false
notifyIf(error, 'group') notifyIf(error, 'group')

View file

@ -199,6 +199,39 @@ export class RealTimeHandler {
store.dispatch('auth/forceUpdateUserData', data.user_data) 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) => { this.registerEvent('group_created', ({ store }, data) => {
store.dispatch('group/forceCreate', data.group) store.dispatch('group/forceCreate', data.group)
}) })
@ -226,6 +259,29 @@ export class RealTimeHandler {
store.dispatch('group/forceOrder', data.group_ids) 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) => { this.registerEvent('application_created', ({ store }, data) => {
store.dispatch('application/forceCreate', data.application) store.dispatch('application/forceCreate', data.application)
}) })

View file

@ -160,9 +160,21 @@ export const actions = {
/** /**
* Updates the account information is the authenticated user. * 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) const { data } = await AuthService(this.$client).update(values)
commit('UPDATE_USER_DATA', { user: data }) commit('UPDATE_USER_DATA', { user: data })
dispatch(
'group/forceUpdateGroupUserAttributes',
{
userId: getters.getUserId,
values: {
name: data.first_name,
},
},
{
root: true,
}
)
return data return data
}, },
forceUpdateUserData({ commit }, data) { forceUpdateUserData({ commit }, data) {

View file

@ -69,6 +69,38 @@ export const mutations = {
}) })
state.selected = {} 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 = { export const actions = {
@ -240,6 +272,77 @@ export const actions = {
}) })
return dispatch('application/clearAll', group, { root: true }) 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 = { 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)
})
})