Skip to content

Commit 8c4e303

Browse files
[rel-db] Fix meeting.import (#3287)
* Fix meeting.import * Fix meeting export * Fix meeting.clone
1 parent 63a4eca commit 8c4e303

8 files changed

Lines changed: 352 additions & 150 deletions

File tree

openslides_backend/action/actions/meeting/clone.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
268268
raise ActionException(
269269
"Cannot create a non-template meeting without administrators"
270270
)
271+
self.transform_json_fields(instance)
271272
return instance
272273

273274
def _create_or_get_meeting_user(self, meeting_id: int, user_id: int) -> int:

openslides_backend/action/actions/meeting/import_.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from datetime import datetime
55
from typing import Any
66

7+
from psycopg.types.json import Jsonb
8+
79
from openslides_backend.action.actions.meeting.mixins import MeetingPermissionMixin
810
from openslides_backend.migrations.migration_helper import MigrationHelper
911
from openslides_backend.models.base import model_registry
@@ -13,9 +15,11 @@
1315
BaseRelationField,
1416
GenericRelationField,
1517
GenericRelationListField,
18+
JSONField,
1619
OnDelete,
1720
RelationField,
1821
RelationListField,
22+
TimestampField,
1923
)
2024
from openslides_backend.models.models import Meeting
2125
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
@@ -161,9 +165,42 @@ def preprocess_data(self, instance: dict[str, Any]) -> dict[str, Any]:
161165
self.remove_not_allowed_fields(instance)
162166
self.set_committee_and_orga_relation(instance)
163167
self.check_data_migration_index(instance)
168+
self.transform_timestamps(instance)
164169
self.unset_committee_and_orga_relation(instance)
165170
return instance
166171

172+
def transform_timestamps(self, instance: dict[str, Any]) -> dict[str, Any]:
173+
for collection, collection_data in instance["meeting"].items():
174+
if model := model_registry.get(collection):
175+
fields = list(model().get_fields())
176+
timestamp_field_names = [
177+
field.own_field_name
178+
for field in fields
179+
if isinstance(field, TimestampField)
180+
]
181+
if timestamp_field_names:
182+
for mod in collection_data.values():
183+
for field in timestamp_field_names:
184+
if (iso := mod.get(field)) and isinstance(iso, str):
185+
mod[field] = datetime.fromisoformat(iso)
186+
return instance
187+
188+
def transform_json_fields(self, instance: dict[str, Any]) -> dict[str, Any]:
189+
for collection, collection_data in instance["meeting"].items():
190+
if model := model_registry.get(collection):
191+
fields = list(model().get_fields())
192+
json_field_names = [
193+
field.own_field_name
194+
for field in fields
195+
if isinstance(field, JSONField)
196+
]
197+
if json_field_names:
198+
for mod in collection_data.values():
199+
for field in json_field_names:
200+
if field in mod:
201+
mod[field] = Jsonb(mod[field])
202+
return instance
203+
167204
def check_one_meeting(self, instance: dict[str, Any]) -> None:
168205
if len(instance["meeting"]["meeting"]) != 1:
169206
raise ActionException("Need exactly one meeting in meeting collection.")
@@ -276,6 +313,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
276313
raise ActionException(str(ce))
277314
self.allowed_collections = checker.allowed_collections
278315

316+
self.transform_json_fields(instance)
279317
self.check_limit_of_meetings()
280318
self.update_meeting_and_users(instance)
281319

@@ -752,7 +790,7 @@ def check_data_migration_index(self, instance: dict[str, Any]) -> None:
752790
"""
753791
Check for valid migration index.
754792
"""
755-
start_migration_index = instance["meeting"].pop("_migration_index")
793+
start_migration_index = instance["meeting"].get("_migration_index")
756794
backend_migration_index = MigrationHelper.get_backend_migration_index()
757795
if backend_migration_index < start_migration_index:
758796
raise ActionException(

openslides_backend/models/fields.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from decimal import Decimal
2+
from decimal import Decimal, InvalidOperation
33
from enum import StrEnum
44
from typing import Any, cast
55

@@ -253,7 +253,12 @@ def validate(self, value: Any, payload: dict[str, Any] = {}) -> Any:
253253
if value is not None or self.required:
254254
if (min_ := self.constraints.get("minimum")) is not None:
255255
if isinstance(value, str):
256-
value = Decimal(value)
256+
try:
257+
value = Decimal(value)
258+
except InvalidOperation:
259+
raise ActionException(
260+
f"{self.own_field_name}: value '{value}' couldn't be converted to decimal."
261+
)
257262
elif not isinstance(value, Decimal | None):
258263
raise NotImplementedError(
259264
f"Unexpected type: {type(value)} (value: {value}) for field {self.get_own_field_name()}"

openslides_backend/presenter/export_meeting.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ def get_result(self) -> Any:
4040
msg = "You are not allowed to perform presenter export_meeting."
4141
msg += f" Missing permission: {OrganizationManagementLevel.SUPERADMIN}"
4242
raise PermissionDenied(msg)
43-
export_data = export_meeting(self.datastore, self.data["meeting_id"])
43+
export_data = export_meeting(
44+
self.datastore, self.data["meeting_id"], datetime_decimal_to_string=True
45+
)
4446
if id_ := next(
4547
(
4648
id_

openslides_backend/shared/export_helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import datetime
12
from collections.abc import Iterable
3+
from decimal import Decimal
24
from typing import Any
35

46
from openslides_backend.migrations.migration_helper import MigrationHelper
@@ -34,6 +36,7 @@ def export_meeting(
3436
meeting_id: int,
3537
internal_target: bool = False,
3638
update_mediafiles: bool = False,
39+
datetime_decimal_to_string: bool = False,
3740
) -> dict[str, Any]:
3841
export: dict[str, Any] = {}
3942

@@ -212,6 +215,14 @@ def export_meeting(
212215
export[collection] = dict(
213216
sorted(instances.items(), key=lambda item: int(item[0]))
214217
)
218+
if datetime_decimal_to_string and isinstance(instances, dict):
219+
for data in instances.values():
220+
for field, value in data.items():
221+
if isinstance(value, datetime.datetime):
222+
data[field] = value.isoformat()
223+
if isinstance(value, Decimal):
224+
data[field] = str(value)
225+
215226
return export
216227

217228

tests/system/action/meeting/test_clone.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,11 +1372,11 @@ def test_meeting_name_exact_fit(self) -> None:
13721372
self.assert_model_exists("meeting/2", {"name": long_name + " - Copy"})
13731373

13741374
def test_meeting_name_too_long(self) -> None:
1375-
self.meeting_data["name"] = "A" * 100
1375+
self.meeting_data["name"] = "A" * 200
13761376
self.set_test_data_with_admin()
13771377
response = self.request("meeting.clone", {"meeting_id": 1})
13781378
self.assert_status_code(response, 200)
1379-
self.assert_model_exists("meeting/2", {"name": "A" * 90 + "... - Copy"})
1379+
self.assert_model_exists("meeting/2", {"name": "A" * 190 + "... - Copy"})
13801380

13811381
def test_permissions_explicit_source_committee_permission(self) -> None:
13821382
self.set_test_data()
@@ -1841,6 +1841,21 @@ def test_clone_amendment_paragraphs(self) -> None:
18411841
response.json["message"],
18421842
)
18431843

1844+
def test_clone_amendment_paragraphs_regular(self) -> None:
1845+
self.set_test_data()
1846+
self.set_user_groups(1, [1])
1847+
self.create_motion(
1848+
1, 1, motion_data={"amendment_paragraphs": Jsonb({"1": "<p>test</p>"})}
1849+
)
1850+
response = self.request(
1851+
"meeting.clone",
1852+
{
1853+
"meeting_id": 1,
1854+
"admin_ids": [1],
1855+
},
1856+
)
1857+
self.assert_status_code(response, 200)
1858+
18441859
def test_permissions_oml_locked_meeting(self) -> None:
18451860
self.create_meeting(
18461861
meeting_data={"locked_from_inside": True, "template_for_organization_id": 1}
@@ -2134,3 +2149,15 @@ def test_clone_with_structured_published_orga_files(self) -> None:
21342149
for fqid, model in models.items():
21352150
self.assert_model_exists(fqid, model)
21362151
self.media.duplicate_mediafile.assert_not_called()
2152+
2153+
def test_clone_require_duplicate_from_allowed(self) -> None:
2154+
self.set_test_data_with_admin()
2155+
self.set_models(
2156+
{
2157+
"meeting/1": {"template_for_organization_id": 1, "name": "m1"},
2158+
}
2159+
)
2160+
self.set_committee_management_level([60])
2161+
self.set_organization_management_level(None)
2162+
response = self.request("meeting.clone", {"meeting_id": 1})
2163+
self.assert_status_code(response, 200)

0 commit comments

Comments
 (0)