Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce speaker deletion logic to user.update and user.delete #1921

Merged
2 changes: 0 additions & 2 deletions global/meta/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,6 @@ meeting_user:
speaker_ids:
type: relation-list
to: speaker/meeting_user_id
on_delete: CASCADE
restriction_mode: A
supported_motion_ids:
type: relation-list
Expand Down Expand Up @@ -2118,7 +2117,6 @@ speaker:
meeting_user_id:
type: relation
to: meeting_user/speaker_ids
required: true
equal_fields: meeting_id
restriction_mode: A
point_of_order_category_id:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import Any, Dict, Optional

from openslides_backend.shared.filters import And, FilterOperator

from ....services.datastore.commands import GetManyRequest
from ...action import Action
from ..speaker.delete import SpeakerDeleteAction


class ConditionalSpeakerCascadeMixin(Action):
"""
Mixin for user actions that deletes unstarted speeches of users that were either deleted, or removed from a meeting
"""

def update_instance(self, instance: Dict[str, Any]) -> Dict[str, Any]:
removed_meeting_id = self.get_removed_meeting_id(instance)
if removed_meeting_id is not None:
filter_: Any = FilterOperator("user_id", "=", instance["id"])
if removed_meeting_id:
filter_ = And(
filter_, FilterOperator("meeting_id", "=", removed_meeting_id)
)
meeting_users = self.datastore.filter(
"meeting_user", filter_, ["speaker_ids"]
)
speaker_ids = [
speaker_id
for val in meeting_users.values()
if val.get("speaker_ids")
for speaker_id in val.get("speaker_ids", [])
]
speakers = self.datastore.get_many(
[
GetManyRequest(
"speaker",
speaker_ids,
[
"begin_time",
"id",
],
)
]
)
speakers_to_delete = [
speaker
for speaker in speakers.get("speaker", {}).values()
if speaker.get("begin_time") is None
]

if len(speakers_to_delete):
self.execute_other_action(
SpeakerDeleteAction,
[{"id": speaker["id"]} for speaker in speakers_to_delete],
)

return super().update_instance(instance)

def get_removed_meeting_id(self, instance: Dict[str, Any]) -> Optional[int]:
"""
Get the id of the meetings from which the user is removed.
If the user is removed from all meetings, the return value will be 0.
If the user is removed from no meetings, it will be None.
"""
raise NotImplementedError()
10 changes: 7 additions & 3 deletions openslides_backend/action/actions/user/delete.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import Any, Dict
from typing import Any, Dict, Optional

from ....models.models import User
from ....shared.exceptions import ActionException
from ....shared.mixins.user_scope_mixin import UserScopeMixin
from ...generics.delete import DeleteAction
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin


@register_action("user.delete")
class UserDelete(UserScopeMixin, DeleteAction):
class UserDelete(UserScopeMixin, ConditionalSpeakerCascadeMixin, DeleteAction):
"""
Action to delete a user.
"""
Expand All @@ -22,7 +23,10 @@ class UserDelete(UserScopeMixin, DeleteAction):
def update_instance(self, instance: Dict[str, Any]) -> Dict[str, Any]:
if instance["id"] == self.user_id:
raise ActionException("You cannot delete yourself.")
return instance
return super().update_instance(instance)

def check_permissions(self, instance: Dict[str, Any]) -> None:
self.check_permissions_for_scope(instance["id"])

def get_removed_meeting_id(self, instance: Dict[str, Any]) -> Optional[int]:
return 0
9 changes: 8 additions & 1 deletion openslides_backend/action/actions/user/update.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any, Dict, Optional

from ....models.models import User
from ....permissions.management_levels import OrganizationManagementLevel
Expand All @@ -9,6 +9,7 @@
from ...mixins.send_email_mixin import EmailCheckMixin
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin
from .create_update_permissions_mixin import CreateUpdatePermissionsMixin
from .user_mixin import (
LimitOfUserMixin,
Expand All @@ -25,6 +26,7 @@ class UserUpdate(
UpdateAction,
LimitOfUserMixin,
UpdateHistoryMixin,
ConditionalSpeakerCascadeMixin,
):
"""
Action to update a user.
Expand Down Expand Up @@ -103,3 +105,8 @@ def update_instance(self, instance: Dict[str, Any]) -> Dict[str, Any]:

check_gender_helper(self.datastore, instance)
return instance

def get_removed_meeting_id(self, instance: Dict[str, Any]) -> Optional[int]:
if instance.get("group_ids") == []:
return instance.get("meeting_id")
return None
8 changes: 3 additions & 5 deletions openslides_backend/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .base import Model
from .mixins import AgendaItemModelMixin, MeetingModelMixin, PollModelMixin

MODELS_YML_CHECKSUM = "566944b1e2fa1b4b216537f3acbb3e5f"
MODELS_YML_CHECKSUM = "34d6dfe51212a0566f81e5c2f67675d6"


class Organization(Model):
Expand Down Expand Up @@ -157,9 +157,7 @@ class MeetingUser(Model):
personal_note_ids = fields.RelationListField(
to={"personal_note": "meeting_user_id"}, on_delete=fields.OnDelete.CASCADE
)
speaker_ids = fields.RelationListField(
to={"speaker": "meeting_user_id"}, on_delete=fields.OnDelete.CASCADE
)
speaker_ids = fields.RelationListField(to={"speaker": "meeting_user_id"})
supported_motion_ids = fields.RelationListField(
to={"motion": "supporter_meeting_user_ids"}
)
Expand Down Expand Up @@ -1051,7 +1049,7 @@ class Speaker(Model):
to={"list_of_speakers": "speaker_ids"}, required=True, equal_fields="meeting_id"
)
meeting_user_id = fields.RelationField(
to={"meeting_user": "speaker_ids"}, required=True, equal_fields="meeting_id"
to={"meeting_user": "speaker_ids"}, equal_fields="meeting_id"
)
point_of_order_category_id = fields.RelationField(
to={"point_of_order_category": "speaker_ids"}, equal_fields="meeting_id"
Expand Down
18 changes: 15 additions & 3 deletions tests/system/action/user/test_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,30 @@ def test_delete_with_speaker(self) -> None:
"meeting_user/1111": {
"meeting_id": 1,
"user_id": 111,
"speaker_ids": [15],
"speaker_ids": [15, 16],
},
"meeting/1": {},
"speaker/15": {"meeting_user_id": 1111, "meeting_id": 1},
"speaker/15": {
"meeting_user_id": 1111,
"meeting_id": 1,
"begin_time": 12345678,
},
"speaker/16": {
"meeting_user_id": 1111,
"meeting_id": 1,
},
}
)
response = self.request("user.delete", {"id": 111})

self.assert_status_code(response, 200)
self.assert_model_deleted("user/111")
self.assert_model_deleted("meeting_user/1111")
self.assert_model_deleted("speaker/15")
self.assert_model_exists(
"speaker/15",
{"meeting_user_id": None, "meeting_id": 1, "begin_time": 12345678},
)
self.assert_model_deleted("speaker/16")

def test_delete_with_candidate(self) -> None:
self.set_models(
Expand Down
121 changes: 121 additions & 0 deletions tests/system/action/user/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2068,3 +2068,124 @@ def test_update_saml_id_default_password_error(self) -> None:
"user 111 is a Single Sign On user and may not set the local default_passwort or the right to change it locally.",
response.json["message"],
)

def test_group_removal_with_speaker(self) -> None:
luisa-beerboom marked this conversation as resolved.
Show resolved Hide resolved
self.set_models(
{
"user/1234": {
"username": "username_abcdefgh123",
"meeting_user_ids": [4444, 5555],
},
"meeting_user/4444": {
"meeting_id": 4,
"user_id": 1234,
"speaker_ids": [14, 24],
"group_ids": [42],
},
"meeting_user/5555": {
"meeting_id": 5,
"user_id": 1234,
"speaker_ids": [25],
"group_ids": [53],
},
"meeting/4": {
"is_active_in_organization_id": 1,
"meeting_user_ids": [4444],
"committee_id": 1,
},
"meeting/5": {
"is_active_in_organization_id": 1,
"meeting_user_ids": [5555],
"committee_id": 1,
},
"committee/1": {"meeting_ids": [4, 5]},
"speaker/14": {"meeting_user_id": 4444, "meeting_id": 4},
"speaker/24": {
"meeting_user_id": 4444,
"meeting_id": 4,
"begin_time": 987654321,
},
"speaker/25": {"meeting_user_id": 5555, "meeting_id": 5},
"group/42": {"meeting_id": 4, "meeting_user_ids": [4444]},
"group/53": {"meeting_id": 5, "meeting_user_ids": [5555]},
}
)
response = self.request(
"user.update", {"id": 1234, "group_ids": [], "meeting_id": 4}
)

self.assert_status_code(response, 200)
self.assert_model_exists(
"user/1234",
{
"username": "username_abcdefgh123",
"meeting_user_ids": [4444, 5555],
},
)
self.assert_model_exists(
"meeting_user/4444",
{"group_ids": [], "speaker_ids": [24], "meta_deleted": False},
)
self.assert_model_exists(
"meeting_user/5555",
{"group_ids": [53], "speaker_ids": [25], "meta_deleted": False},
)
self.assert_model_exists(
"speaker/24", {"meeting_user_id": 4444, "meeting_id": 4}
)
self.assert_model_exists(
"speaker/25", {"meeting_user_id": 5555, "meeting_id": 5}
)
self.assert_model_deleted("speaker/14")

def test_partial_group_removal_with_speaker(self) -> None:
self.set_models(
{
"user/1234": {
"username": "username_abcdefgh123",
"meeting_user_ids": [4444],
},
"meeting_user/4444": {
"meeting_id": 4,
"user_id": 1234,
"speaker_ids": [14, 24],
"group_ids": [42, 43],
},
"meeting/4": {
"is_active_in_organization_id": 1,
"meeting_user_ids": [4444],
"committee_id": 1,
},
"committee/1": {"meeting_ids": [4]},
"speaker/14": {"meeting_user_id": 4444, "meeting_id": 4},
"speaker/24": {
"meeting_user_id": 4444,
"meeting_id": 4,
"begin_time": 987654321,
},
"group/42": {"meeting_id": 4, "meeting_user_ids": [4444]},
"group/43": {"meeting_id": 4, "meeting_user_ids": [4444]},
}
)
response = self.request(
"user.update", {"id": 1234, "group_ids": [43], "meeting_id": 4}
)

self.assert_status_code(response, 200)
self.assert_model_exists(
"user/1234",
{
"username": "username_abcdefgh123",
"meeting_user_ids": [4444],
},
)
self.assert_model_exists(
"meeting_user/4444",
{"group_ids": [43], "speaker_ids": [14, 24], "meta_deleted": False},
)
self.assert_model_exists(
"speaker/24", {"meeting_user_id": 4444, "meeting_id": 4}
)
self.assert_model_exists(
"speaker/14", {"meeting_user_id": 4444, "meeting_id": 4}
)