Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
26639b4
Require groups in meeting users, fix most tests
luisa-beerboom Nov 12, 2025
a7654e4
Merge remote-tracking branch 'upstream/main' into 2111-require-groups…
luisa-beerboom Nov 12, 2025
66b71e7
Make user.update delete meeting_users
luisa-beerboom Nov 13, 2025
9b9d24f
Finish (?) user.update changes
luisa-beerboom Nov 13, 2025
2bb5270
Make changes for motions
luisa-beerboom Nov 14, 2025
96018df
Make changes for chat messages
luisa-beerboom Nov 14, 2025
c4791c2
Make changes for personal note
luisa-beerboom Nov 14, 2025
b188ffa
Update docs
luisa-beerboom Nov 14, 2025
7963c7e
Fix clone test
luisa-beerboom Nov 17, 2025
281c37a
Merge remote-tracking branch 'upstream/main' into 2111-require-groups…
luisa-beerboom Nov 17, 2025
ecb09cc
Write and test migration
luisa-beerboom Nov 17, 2025
a9f7eba
Fix code
luisa-beerboom Nov 18, 2025
1847e4a
Finish first test, no supporter code for now though
luisa-beerboom Nov 18, 2025
69d1d75
motion_supporter test code
luisa-beerboom Nov 19, 2025
7121e2f
Merge remote-tracking branch 'upstream/main' into 2111-require-groups…
luisa-beerboom Nov 27, 2025
df13ae7
Fix migration after update
luisa-beerboom Nov 27, 2025
1635b50
cleanup
luisa-beerboom Nov 27, 2025
306ba2a
More tests
luisa-beerboom Dec 1, 2025
c2dea98
Minor changes
luisa-beerboom Jan 6, 2026
3570c97
Requested code style changes
luisa-beerboom Jan 7, 2026
3fc5a29
Merge remote-tracking branch 'upstream/main' into 2111-require-groups…
luisa-beerboom Jan 7, 2026
32958a0
Fix motion create perms
luisa-beerboom Jan 7, 2026
34c37e9
Add test for issue 3133
luisa-beerboom Jan 8, 2026
d56cd28
Merge remote-tracking branch 'upstream/main' into 2111-require-groups…
luisa-beerboom Jan 14, 2026
42fcde9
Requested changes
luisa-beerboom Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data/example-data.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_migration_index": 74,
"_migration_index": 75,
"gender":{
"1":{
"id": 1,
Expand Down
2 changes: 1 addition & 1 deletion data/initial-data.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_migration_index": 74,
"_migration_index": 75,
"gender":{
"1":{
"id": 1,
Expand Down
3 changes: 2 additions & 1 deletion docs/actions/chat_message.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ Creates a new `chat_message` for the `chat_group` given by the key `chat_group_i

## Permission

Every user, who is in one of the write groups of a chat group, or has the permission `chat.can_manage` can create a `chat_message`.
Every user, who is in one of the write groups of a chat group, or has the permission `chat.can_manage` can create a `chat_message`.
User also needs to be part of the meeting.
4 changes: 2 additions & 2 deletions docs/actions/motion.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

// Optional special fields, see notes below
workflow_id: Id;
submitter_ids: Id[];
submitter_meeting_user_ids: Id[];

// Non-model fields for customizing the agenda item creation, optional
agenda_create: boolean;
Expand Down Expand Up @@ -58,7 +58,7 @@ This is the logic for other fields depending on the motion type:
There are some fields that need special attention:
- `workflow_id`: If it is given, the motion's state is set to the workflow's first state. The workflow must be from the same meeting. If the field is not given, one of the three default (`meeting/motions_default_workflow_id` or `meeting/motions_default_amendment_workflow_id`) workflows is used depending on the type of the motion to create.
- `additional_submitter` a text field where text-based submitter information may be entered. Cannot be set unless `meeting/motions_create_enable_additional_submitter_text` is `true`. Requires permissions `Motion.CAN_CREATE` and `Motion.CAN_MANAGE_METADATA`.
- `submitter_ids`: These are **user ids** and not ids of the `motion_submitter` model. If nothing is given (`[]`) and the field `additional_submitter` isn't filled, the request user's id is used. For each id in the list a `motion_submitter` model is created. The weight must be set to the order of the given list. Requires permissions `Motion.CAN_CREATE`, `Motion.CAN_MANAGE_METADATA` and `User.CAN_SEE`.
- `submitter_meeting_user_ids`: These are ids of the meeting users that should get a `motion_submitter` model. Can be left empty. The weight of the new submitters is set to the order of the given list. Requires permissions `Motion.CAN_CREATE`, `Motion.CAN_MANAGE_METADATA` and `User.CAN_SEE` (the latter two only if not setting oneself).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name needs to also be changed in the payload description.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

- `agenda_*`: See [Agenda](https://github.com/OpenSlides/OpenSlides/wiki/Agenda#additional-fields-during-creation-of-agenda-content-objects).

Other things to do when creating motions:
Expand Down
1 change: 1 addition & 0 deletions docs/actions/user.update.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Updates a user.
Note: `is_present_in_meeting_ids` is not available in update, since there is no possibility to partially update this field. This can be done via [user.set_present](user.set_present.md).

If the user is removed from all groups of the meeting, all his unstarted speakers in that meeting will be deleted.
His meeting_user for that meeting will also be deleted.

If the user was the last member of the meetings admin group and he happens to be removed from the latter through this action, as long as the meeting is not a template, there will be an error.

Expand Down
2 changes: 1 addition & 1 deletion meta
Submodule meta updated 1 files
+1 −0 models.yml
12 changes: 8 additions & 4 deletions openslides_backend/action/actions/chat_message/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ....models.models import ChatMessage
from ....permissions.permission_helper import has_perm
from ....permissions.permissions import Permissions
from ....shared.exceptions import PermissionDenied
from ....shared.exceptions import ActionException, PermissionDenied
from ....shared.patterns import fqid_from_collection_and_id
from ...mixins.create_action_with_inferred_meeting import (
CreateActionWithInferredMeeting,
Expand All @@ -28,9 +28,13 @@ class ChatMessageCreate(MeetingUserHelperMixin, CreateActionWithInferredMeeting)

def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
instance = super().update_instance(instance)
instance["meeting_user_id"] = self.create_or_get_meeting_user(
instance["meeting_id"], self.user_id
)
instance["meeting_user_id"] = (
self.get_meeting_user(instance["meeting_id"], self.user_id, ["id"]) or {}
).get("id")
if not instance.get("meeting_user_id"):
raise ActionException(
"Cannot create chat message: You are not a participant of the meeting."
)
instance["created"] = round(time())
return instance

Expand Down
25 changes: 25 additions & 0 deletions openslides_backend/action/actions/meeting_user/base_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from openslides_backend.shared.patterns import fqid_from_collection_and_id
from openslides_backend.shared.typing import HistoryInformation

from ....models.models import MeetingUser
from ...generics.delete import DeleteAction
from ...util.default_schema import DefaultSchema


class MeetingUserBaseDelete(DeleteAction):
"""
Base action to delete a meeting user.
"""

model = MeetingUser()
schema = DefaultSchema(MeetingUser()).get_delete_schema()

def get_history_information(self) -> HistoryInformation | None:
users = self.get_instances_with_fields(["user_id", "meeting_id"])
return {
fqid_from_collection_and_id("user", user["user_id"]): [
"Participant removed from meeting {}",
fqid_from_collection_and_id("meeting", user["meeting_id"]),
]
for user in users
}
20 changes: 2 additions & 18 deletions openslides_backend/action/actions/meeting_user/delete.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
from typing import Any

from openslides_backend.shared.patterns import fqid_from_collection_and_id
from openslides_backend.shared.typing import HistoryInformation

from ....models.models import MeetingUser
from ...generics.delete import DeleteAction
from ...util.action_type import ActionType
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from ..user.conditional_speaker_cascade_mixin import (
ConditionalSpeakerCascadeMixinHelper,
)
from .base_delete import MeetingUserBaseDelete


@register_action("meeting_user.delete", action_type=ActionType.BACKEND_INTERNAL)
class MeetingUserDelete(ConditionalSpeakerCascadeMixinHelper, DeleteAction):
class MeetingUserDelete(ConditionalSpeakerCascadeMixinHelper, MeetingUserBaseDelete):
"""
Action to delete a meeting user.
"""

model = MeetingUser()
schema = DefaultSchema(MeetingUser()).get_delete_schema()

def get_history_information(self) -> HistoryInformation | None:
users = self.get_instances_with_fields(["user_id", "meeting_id"])
return {
fqid_from_collection_and_id("user", user["user_id"]): [
"Participant removed from meeting {}",
fqid_from_collection_and_id("meeting", user["meeting_id"]),
]
for user in users
}

def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
meeting_user = self.datastore.get(
fqid_from_collection_and_id("meeting_user", instance["id"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def handle_group_updates(
group_information: list[str] = []
if added and removed:
group_information.append("Groups changed")
else:
elif added or removed:
if added:
group_information.append("Participant added to")
else:
Expand All @@ -272,7 +272,8 @@ def handle_group_updates(
group_information.append(
fqid_from_collection_and_id("meeting", meeting_id)
)
instance_information.append(tuple(group_information))
if group_information:
instance_information.append(tuple(group_information))

def handle_delegations(
self,
Expand Down
40 changes: 37 additions & 3 deletions openslides_backend/action/actions/motion/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class MotionCreate(
required_properties=["meeting_id", "title"],
additional_optional_fields={
"workflow_id": optional_id_schema,
"submitter_ids": id_list_schema,
"submitter_meeting_user_ids": id_list_schema,
"amendment_paragraphs": number_string_json_schema,
"attachment_mediafile_ids": id_list_schema,
"supporter_meeting_user_ids": id_list_schema,
Expand Down Expand Up @@ -148,6 +148,41 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
if not has_perm(self.datastore, self.user_id, perm, instance["meeting_id"]):
raise MissingPermission(perm)

extra_submitter_perms: list[Permission] = [
Permissions.User.CAN_SEE,
Permissions.Motion.CAN_MANAGE_METADATA,
]
if (
(submitter_mu_ids := instance.get("submitter_meeting_user_ids"))
and (
len(submitter_mu_ids) > 1
or (
submitter_mu_ids[0]
!= (
self.get_meeting_user(
instance["meeting_id"], self.user_id, ["id"]
)
or {}
).get("id")
)
)
and len(
missing_perms := {
perm: instance["meeting_id"]
for perm in extra_submitter_perms
if not has_perm(
self.datastore,
self.user_id,
perm,
instance["meeting_id"],
)
}
)
):
raise MissingPermission(
{key: val for key, val in missing_perms.items()}, use_and=True
)

# Whitelist the fields depending on the user's permissions. Each field can require multiple conjunctive permissions.
can_manage_whitelist = set()
forbidden_fields = defaultdict(set)
Expand All @@ -156,9 +191,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
Permissions.Mediafile.CAN_SEE: ["attachment_mediafile_ids"],
Permissions.Motion.CAN_MANAGE_METADATA: [
"additional_submitter",
"submitter_ids",
],
Permissions.User.CAN_SEE: ["submitter_ids"],
}
for perm, fields in permission_to_fields.items():
has_permission = has_perm(
Expand Down Expand Up @@ -191,6 +224,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
"workflow_id",
"id",
"meeting_id",
"submitter_meeting_user_ids",
]
)
if instance.get("lead_motion_id"):
Expand Down
9 changes: 2 additions & 7 deletions openslides_backend/action/actions/motion/create_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,10 @@ def set_state_from_workflow(
)

def create_submitters(self, instance: dict[str, Any]) -> None:
submitter_ids = instance.pop("submitter_ids", [])
if not submitter_ids and not instance.get("additional_submitter"):
submitter_ids = [self.user_id]
submitter_ids = instance.pop("submitter_meeting_user_ids", [])
self.apply_instance(instance)
weight = 1
for user_id in submitter_ids:
meeting_user_id = self.create_or_get_meeting_user(
instance["meeting_id"], user_id
)
for meeting_user_id in submitter_ids:
data = {
"motion_id": instance["id"],
"meeting_user_id": meeting_user_id,
Expand Down
17 changes: 2 additions & 15 deletions openslides_backend/action/actions/personal_note/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
)
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from ..meeting_user.create import MeetingUserCreate
from .mixins import PermissionMixin


Expand Down Expand Up @@ -59,20 +58,8 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
filtered_meeting_user = self.datastore.filter(
"meeting_user", filter_, ["id", "personal_note_ids"]
)
if filtered_meeting_user:
meeting_user = list(filtered_meeting_user.values())[0]
instance["meeting_user_id"] = meeting_user["id"]
else:
action_results = self.execute_other_action(
MeetingUserCreate,
[
{
"user_id": self.user_id,
"meeting_id": instance["meeting_id"],
}
],
)
instance["meeting_user_id"] = action_results[0]["id"] # type: ignore
meeting_user = list(filtered_meeting_user.values())[0]
instance["meeting_user_id"] = meeting_user["id"]

if not (instance.get("star") or instance.get("note")):
raise ActionException("Can't create personal note without star or note.")
Expand Down
20 changes: 20 additions & 0 deletions openslides_backend/action/actions/user/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from ....shared.patterns import fqid_from_collection_and_id
from ....shared.schema import optional_id_schema
from ...generics.update import UpdateAction
from ...mixins.meeting_user_helper import get_meeting_user_filter
from ...mixins.send_email_mixin import EmailCheckMixin
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from ..meeting_user.base_delete import MeetingUserBaseDelete
from ..meeting_user.mixin import CheckLockOutPermissionMixin
from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin
from .user_mixins import (
Expand All @@ -29,6 +31,14 @@
)


class MeetingUserDeleteInternal(MeetingUserBaseDelete):
"""
Action to delete a meeting user.
"""

name = "meeting_user.delete_internal_helper"


@register_action("user.update")
class UserUpdate(
UserMixin,
Expand Down Expand Up @@ -116,7 +126,17 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
self.check_locking_status(
instance.get("meeting_id"), instance, instance["id"], None
)
removed_meeting_id = self.get_removed_meeting_id(instance)
instance = super().update_instance(instance)
if removed_meeting_id:
meeting_users = self.datastore.filter(
"meeting_user",
get_meeting_user_filter(removed_meeting_id, instance["id"]),
[],
)
self.execute_other_action(
MeetingUserDeleteInternal, [{"id": id_} for id_ in meeting_users]
)
home_committee_id = instance.get("home_committee_id")
user = self.datastore.get(
fqid_from_collection_and_id("user", instance["id"]),
Expand Down
Loading