From ee7cdf169c31935392c0d079582d173157bc7c8f Mon Sep 17 00:00:00 2001 From: Luisa Date: Mon, 25 Nov 2024 14:26:11 +0100 Subject: [PATCH] Remove motion import --- docs/actions/motion.import.md | 21 - docs/actions/motion.json_upload.md | 54 - .../action/actions/motion/__init__.py | 2 - .../action/actions/motion/import_.py | 635 --------- .../action/actions/motion/json_upload.py | 599 --------- tests/system/action/motion/test_import.py | 1161 ----------------- .../system/action/motion/test_json_upload.py | 974 -------------- 7 files changed, 3446 deletions(-) delete mode 100644 docs/actions/motion.import.md delete mode 100644 docs/actions/motion.json_upload.md delete mode 100644 openslides_backend/action/actions/motion/import_.py delete mode 100644 openslides_backend/action/actions/motion/json_upload.py delete mode 100644 tests/system/action/motion/test_import.py delete mode 100644 tests/system/action/motion/test_json_upload.py diff --git a/docs/actions/motion.import.md b/docs/actions/motion.import.md deleted file mode 100644 index d93d50c8e..000000000 --- a/docs/actions/motion.import.md +++ /dev/null @@ -1,21 +0,0 @@ -## Payload -```js -{ -// required - id: Id; // action worker id - import: boolean; -} -``` - - -## Action -If `import` is `True`, use the rows from the given action worker and check that the import type -matches and whether it should still be created (row state `new`) or update (row state `done`). -On row state `new`, the username must not exist yet. On row state `done`, -the record with the matching `id` should still have the same username. On error, don't import anything, -but create data as in json_upload. Do the actual import with bulk actions. - -If `import` is `False` or the import was successful, remove the action worker. - -## Permission -The request user needs permission `motion.can_manage`, but only allow importing data if there are no errors in preview. \ No newline at end of file diff --git a/docs/actions/motion.json_upload.md b/docs/actions/motion.json_upload.md deleted file mode 100644 index eae90a8ac..000000000 --- a/docs/actions/motion.json_upload.md +++ /dev/null @@ -1,54 +0,0 @@ -## Payload - -Because the data fields are all converted from CSV import file, **they are all of type `string`**. -The types noted below are the internal types after conversion in the backend. See [here](preface_special_imports.md#internal-types) for the representation of the types. -```js -{ - // required for new motions - data: { - // required in create - title: string, // info: done, error - text: string, // info: done, error - // all optional, but see rules below - number: string, // unique when set, info: done, generated or error - reason: string, // required for create if the meeting has "motions_reason_required", info: done or error - submitters_verbose: string[], - submitters_username: string[], // info: done, generated, warning, error if len(submitters_verbose) > len(submitters_username) - supporters_verbose: string[], - supporters_username: string[], // info: done, warning, error if len(supporters_verbose) > len(supporters_username) - category_name: string, // info: done or warning, partial reference to: motion_category - category_prefix: string, - tags: string[], // info: done or warning, reference to: tag - block: string, // info: done or warning, reference to: motion_block - motion_amendment: boolean, // info: done or warning, if True, warning, that motion amendments cannot be imported - }[]; - meeting_id: Id, // id of the current meeting. -} -``` -## Return value and object fields - -Besides the usual headers as seen in payload (name and type), there are these differences: - -- `submitters`, `supporter_users`, `category_name/prefix`, `tags` and `block`: Objects that show if the model has been found (`done`) or not (`warning`). -- `text`: will be surrounded in html `

` tags if the string isn't encased in html tags already. -- `number`: will be object with error, if the field is set and another row in the payload has the same number. If the `number` field is left empty and the motion is going to be created in a `motion_state` where `set_number` is true, a new `number` will be generated and the object is going to have the info `generated`. - -The row state can be one of "new", "done" or "error". In case of an error, no import should be possible. - -See [common description](preface_special_imports.md#general-format-of-the-result-send-to-the-client-for-preview). - -Other than the validity check for the username-fields, `submitters_verbose` and `supporters_verbose` are NOT otherwise used or taken note of in the import. They are merely accepted in order to check that someone didn't accidentally edit the wrong column in a file that has both verbose and non-verbose columns. -They are not included in the return value. - - -## Action -The data will create or update motions. - -### Motion matching - -Motions can be updated via their `number`. -If a motion has a `number`, it will be matched with and updated with the data of any import date that has the same `number`. -Therefore motions that don't have a number can not be overwritten. - -## Permission -Permission `motion.can_manage` \ No newline at end of file diff --git a/openslides_backend/action/actions/motion/__init__.py b/openslides_backend/action/actions/motion/__init__.py index f4b731bfe..9f504ec2a 100644 --- a/openslides_backend/action/actions/motion/__init__.py +++ b/openslides_backend/action/actions/motion/__init__.py @@ -3,8 +3,6 @@ create_forwarded, delete, follow_recommendation, - import_, - json_upload, reset_recommendation, reset_state, set_recommendation, diff --git a/openslides_backend/action/actions/motion/import_.py b/openslides_backend/action/actions/motion/import_.py deleted file mode 100644 index 3e17e5d6c..000000000 --- a/openslides_backend/action/actions/motion/import_.py +++ /dev/null @@ -1,635 +0,0 @@ -from typing import Any, cast - -from openslides_backend.action.mixins.import_mixins import ( - BaseImportAction, - ImportRow, - ImportState, - Lookup, - ResultType, -) -from openslides_backend.action.util.register import register_action -from openslides_backend.permissions.permissions import Permissions -from openslides_backend.shared.exceptions import ActionException -from openslides_backend.shared.filters import And, Filter, FilterOperator, Or - -from ....models.models import ImportPreview -from ....shared.patterns import fqid_from_collection_and_id -from ....shared.schema import required_id_schema -from ...util.default_schema import DefaultSchema -from ..meeting_user.create import MeetingUserCreate -from ..motion_submitter.create import MotionSubmitterCreateAction -from ..motion_submitter.delete import MotionSubmitterDeleteAction -from ..motion_submitter.sort import MotionSubmitterSort -from .create import MotionCreate -from .payload_validation_mixin import ( - MotionActionErrorData, - MotionCreatePayloadValidationMixin, - MotionErrorType, - MotionUpdatePayloadValidationMixin, -) -from .update import MotionUpdate - - -@register_action("motion.import") -class MotionImport( - BaseImportAction, - MotionCreatePayloadValidationMixin, - MotionUpdatePayloadValidationMixin, -): - """ - Action to import a result from the import_preview. - """ - - model = ImportPreview() - schema = DefaultSchema(ImportPreview()).get_default_schema( - additional_required_fields={ - "id": required_id_schema, - "import": {"type": "boolean"}, - } - ) - permission = Permissions.Motion.CAN_MANAGE - skip_archived_meeting_check = True - import_name = "motion" - number_lookup: Lookup - username_lookup: dict[str, list[dict[str, Any]]] - category_lookup: dict[str, list[dict[str, Any]]] - tags_lookup: dict[str, list[dict[str, Any]]] - block_lookup: dict[str, list[dict[str, Any]]] - _user_ids_to_meeting_user: dict[int, Any] - _submitter_ids_to_user_id: dict[int, int] - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - if not instance["import"]: - return {} - - instance = super().update_instance(instance) - meeting_id = self.get_meeting_id(instance) - self.setup_lookups(meeting_id) - - self.rows = [self.validate_entry(row) for row in self.result["rows"]] - - if self.import_state != ImportState.ERROR: - create_action_payload: list[dict[str, Any]] = [] - update_action_payload: list[dict[str, Any]] = [] - submitter_create_action_payload: list[dict[str, Any]] = [] - submitter_delete_action_payload: list[dict[str, Any]] = [] - - motion_to_submitter_user_ids: dict[int, list[int]] = {} - old_submitters: dict[int, dict[int, int]] = ( - {} - ) # {motion_id: {user_id:submitter_id}} - for row in self.rows: - payload: dict[str, Any] = row["data"].copy() - used_list = ["text", "reason", "title", "number"] - for field in used_list: - if field in payload: - if type(dvalue := payload[field]) is dict: - payload[field] = dvalue["value"] - self.remove_fields_from_data( - payload, - ["submitters_verbose", "supporters_verbose", "motion_amendment"], - ) - if (category := payload.pop("category_name", None)) and category[ - "info" - ] == ImportState.DONE: - payload["category_id"] = ( - category["id"] if category.get("id") else None - ) - if (block := payload.pop("block", None)) and block[ - "info" - ] == ImportState.DONE: - payload["block_id"] = block["id"] if block.get("id") else None - payload["tag_ids"] = self.get_ids_from_object_list( - payload.pop("tags", []) - ) - meeting_users_to_create = [ - {"user_id": submitter["id"], "meeting_id": meeting_id} - for submitter in payload["submitters_username"] - if submitter["info"] == ImportState.GENERATED - and submitter["id"] not in self._user_ids_to_meeting_user.keys() - ] - if len(meeting_users_to_create): - meeting_users = cast( - list[dict[str, int]], - self.execute_other_action( - MeetingUserCreate, meeting_users_to_create - ), - ) - for i in range(len(meeting_users)): - self._user_ids_to_meeting_user[ - meeting_users_to_create[i]["user_id"] - ] = meeting_users[i] - submitters = self.get_ids_from_object_list( - payload.pop("submitters_username") - ) - supporters = [ - self._user_ids_to_meeting_user[supporter_id]["id"] - for supporter_id in self.get_ids_from_object_list( - payload.pop("supporters_username", []) - ) - ] - payload["supporter_meeting_user_ids"] = supporters - payload.pop("category_prefix", None) - errors: list[MotionActionErrorData] = [] - if row["state"] == ImportState.NEW: - payload.update({"submitter_ids": submitters}) - create_action_payload.append(payload) - errors = self.get_create_payload_integrity_error_message( - payload, meeting_id - ) - else: - id_ = payload["id"] - motion_to_submitter_user_ids[id_] = submitters - motion = { - k: v - for k, v in ( - [ - motion - for motion in self.number_lookup.name_to_ids.get( - payload["number"], [] - ) - if motion.get("id") - ][0] - ).items() - } - for field in ["category_id", "block_id"]: - if payload.get(field) is None: - if not motion.get(field): - payload.pop(field, None) - if len(submitters): - motion_submitter_ids: list[int] = ( - motion.get("submitter_ids", []) or [] - ) - matched_submitters = { - self._submitter_ids_to_user_id[submitter_id]: submitter_id - for submitter_id in motion_submitter_ids - if self._submitter_ids_to_user_id.get(submitter_id) - in submitters - } - submitter_create_action_payload.extend( - [ - { - "meeting_user_id": self._user_ids_to_meeting_user[ - user_id - ]["id"], - "motion_id": id_, - } - for user_id in submitters - if user_id not in matched_submitters.keys() - ] - ) - submitter_delete_action_payload.extend( - [ - {"id": submitter_id} - for submitter_id in motion_submitter_ids - if submitter_id not in matched_submitters.values() - ] - ) - old_submitters[id_] = matched_submitters - - payload.pop("meeting_id", None) - update_action_payload.append(payload) - errors = self.get_update_payload_integrity_error_message( - payload, meeting_id - ) - for err in errors: - if err["type"] != MotionErrorType.REASON: - raise ActionException("Error: " + err["message"]) - if not ( - row["data"].get("reason") - and isinstance(row["data"]["reason"], dict) - ): - row["data"]["reason"] = { - "value": row["data"].get("reason", ""), - "info": ImportState.ERROR, - } - else: - row["data"]["reason"]["info"] = ImportState.ERROR - row["data"]["reason"].pop("id", 0) - row["messages"].append("Error: " + err["message"]) - row["state"] = ImportState.ERROR - self.import_state = ImportState.ERROR - if self.import_state != ImportState.ERROR: - created_submitters: list[dict[str, int]] = [] - if create_action_payload: - self.execute_other_action(MotionCreate, create_action_payload) - if update_action_payload: - self.execute_other_action(MotionUpdate, update_action_payload) - if len(submitter_create_action_payload): - created_submitters = cast( - list[dict[str, int]], - self.execute_other_action( - MotionSubmitterCreateAction, submitter_create_action_payload - ), - ) - if len(submitter_delete_action_payload): - self.execute_other_action( - MotionSubmitterDeleteAction, submitter_delete_action_payload - ) - new_submitters: dict[int, dict[int, int]] = ( - {} - ) # {motion_id: {meeting_user_id:submitter_id}} - for i in range(len(created_submitters)): - motion_id = submitter_create_action_payload[i]["motion_id"] - new_submitters[motion_id] = { - **new_submitters.get(motion_id, {}), - submitter_create_action_payload[i][ - "meeting_user_id" - ]: created_submitters[i]["id"], - } - sort_payload: list[dict[str, Any]] = [] - for motion_id in motion_to_submitter_user_ids: - sorted_motion_submitter_ids: list[int] = [] - for submitter_user_id in motion_to_submitter_user_ids[motion_id]: - meeting_user_id = cast( - int, self._user_ids_to_meeting_user[submitter_user_id]["id"] - ) - if ( - submitter_user_id - in old_submitters.get(motion_id, {}).keys() - ): - sorted_motion_submitter_ids.append( - old_submitters[motion_id][submitter_user_id] - ) - elif ( - meeting_user_id in new_submitters.get(motion_id, {}).keys() - ): - sorted_motion_submitter_ids.append( - new_submitters[motion_id][meeting_user_id] - ) - else: - raise Exception( - f"Submitter sorting failed due to submitter for user/{submitter_user_id} not being found" - ) - if len(sorted_motion_submitter_ids): - sort_payload.append( - { - "motion_id": motion_id, - "motion_submitter_ids": sorted_motion_submitter_ids, - } - ) - for payload in sort_payload: - self.execute_other_action(MotionSubmitterSort, [payload]) - - return {} - - def get_ids_from_object_list(self, object_list: list[dict[str, Any]]) -> list[int]: - return [ - obj["id"] - for obj in object_list - if obj.get("info") != ImportState.WARNING - and obj.get("info") != ImportState.ERROR - ] - - def remove_fields_from_data( - self, data: dict[str, Any], fieldnames: list[str] - ) -> None: - for fieldname in fieldnames: - data.pop(fieldname, None) - - def validate_entry(self, row: ImportRow) -> ImportRow: - entry = row["data"] - - if ("id" in entry) != ("id" in entry.get("number", {})): - raise ActionException( - f"Invalid JsonUpload data: A data row with state '{ImportState.DONE}' must have an 'id'" - ) - - number = self.get_value_from_union_str_object(entry.get("number")) - if number: - check_result = self.number_lookup.check_duplicate(number) - id_ = cast(int, self.number_lookup.get_field_by_name(number, "id")) - - if check_result == ResultType.FOUND_ID and id_ != 0: - if row["state"] != ImportState.DONE: - row["messages"].append( - f"Error: Row state expected to be '{ImportState.DONE}', but it is '{row['state']}'." - ) - row["state"] = ImportState.ERROR - entry["number"]["info"] = ImportState.ERROR - elif entry["id"] != id_: - row["state"] = ImportState.ERROR - entry["number"]["info"] = ImportState.ERROR - row["messages"].append( - f"Error: Number '{number}' found in different id ({id_} instead of {entry['id']})" - ) - elif check_result == ResultType.FOUND_MORE_IDS: - row["state"] = ImportState.ERROR - entry["number"]["info"] = ImportState.ERROR - row["messages"].append( - f"Error: Number '{number}' is duplicated in import." - ) - elif check_result == ResultType.NOT_FOUND_ANYMORE: - row["messages"].append( - f"Error: Motion {entry['number']['id']} not found anymore for updating motion '{number}'." - ) - row["state"] = ImportState.ERROR - - category_name = self.get_value_from_union_str_object(entry.get("category_name")) - if category_name and entry["category_name"].get("info") == ImportState.DONE: - category_prefix = entry.get("category_prefix") or None - if "id" not in entry["category_name"]: - raise ActionException( - f"Invalid JsonUpload data: A category_name entry with state '{ImportState.DONE}' must have an 'id'" - ) - categories = self.category_lookup.get(category_name, []) - categories = [ - category - for category in categories - if category.get("prefix") == category_prefix - ] - if len(categories) > 0: - if not any( - [ - category.get("id") == entry["category_name"].get("id") - for category in categories - ] - ): - row["messages"].append( - "Error: Category search didn't deliver the same result as in the preview" - ) - entry["category_name"] = { - "value": category_name, - "info": ImportState.ERROR, - } - row["state"] = ImportState.ERROR - else: - entry["category_name"] = { - "value": category_name, - "info": ImportState.ERROR, - } - row["state"] = ImportState.ERROR - row["messages"].append("Error: Category could not be found anymore") - - block = self.get_value_from_union_str_object(entry.get("block")) - if block and entry["block"].get("info") == ImportState.DONE: - if "id" not in entry["block"]: - raise ActionException( - f"Invalid JsonUpload data: A block entry with state '{ImportState.DONE}' must have an 'id'" - ) - found_blocks = self.block_lookup.get(block, []) - if len(found_blocks) > 0: - if not any( - [block.get("id") == entry["block"]["id"] for block in found_blocks] - ): - entry["block"] = {"value": block, "info": ImportState.ERROR} - row["messages"].append( - "Error: Motion block search didn't deliver the same result as in the preview" - ) - row["state"] = ImportState.ERROR - else: - entry["block"] = { - "value": block, - "info": ImportState.ERROR, - } - row["messages"].append("Error: Couldn't find motion block anymore") - row["state"] = ImportState.ERROR - - if isinstance(entry.get("tags"), list): - different: list[str] = [] - not_found: list[str] = [] - for tag_entry in entry.get("tags", []): - tag = self.get_value_from_union_str_object(tag_entry) - if tag and tag_entry.get("info") == ImportState.DONE: - if "id" not in tag_entry: - raise ActionException( - f"Invalid JsonUpload data: A tag entry with state '{ImportState.DONE}' must have an 'id'" - ) - found_tags = self.tags_lookup.get(tag, []) - if len(found_tags) > 0: - if not any( - [tag.get("id") == tag_entry["id"] for tag in found_tags] - ): - tag_entry["info"] = ImportState.ERROR - tag_entry.pop("id") - different.append(tag) - else: - tag_entry["info"] = ImportState.ERROR - tag_entry.pop("id") - not_found.append(tag) - if len(different): - row["messages"].append( - "Error: Tag search didn't deliver the same result as in the preview: " - + ", ".join(different) - ) - row["state"] = ImportState.ERROR - if len(not_found): - row["messages"].append( - "Error: Couldn't find tag anymore: " + ", ".join(not_found) - ) - row["state"] = ImportState.ERROR - - for fieldname in ["submitter", "supporter"]: - if isinstance(entry.get(f"{fieldname}s_username"), list): - different = [] - not_found = [] - for user_entry in entry.get(f"{fieldname}s_username", []): - user = self.get_value_from_union_str_object(user_entry) - if user and ( - user_entry.get("info") == ImportState.DONE - or user_entry.get("info") == ImportState.GENERATED - ): - if "id" not in user_entry: - raise ActionException( - f"Invalid JsonUpload data: A {fieldname} entry with state '{ImportState.DONE}' or '{ImportState.GENERATED}' must have an 'id'" - ) - found_users = self.username_lookup.get(user, []) - if len(found_users) == 1: - if found_users[0].get("id") != user_entry["id"]: - user_entry["info"] = ImportState.ERROR - user_entry.pop("id") - different.append(user) - elif len(found_users) > 1: - raise ActionException( - f"Database corrupt: Found multiple users with the username {user}." - ) - else: - user_entry["info"] = ImportState.ERROR - user_entry.pop("id") - not_found.append(user) - if len(different): - row["messages"].append( - f"Error: {fieldname[0].capitalize() + fieldname[1:]} search didn't deliver the same result as in the preview: " - + ", ".join(different) - ) - row["state"] = ImportState.ERROR - if len(not_found): - row["messages"].append( - f"Error: Couldn't find {fieldname} anymore: " - + ", ".join(not_found) - ) - row["state"] = ImportState.ERROR - - row["messages"] = list(set(row["messages"])) - - if row["state"] == ImportState.ERROR and self.import_state == ImportState.DONE: - self.import_state = ImportState.ERROR - return { - "state": row["state"], - "data": row["data"], - "messages": row.get("messages", []), - } - - def setup_lookups(self, meeting_id: int) -> None: - rows = self.result["rows"] - self.number_lookup = Lookup( - self.datastore, - "motion", - [ - (entry["number"]["value"], entry) - for row in rows - if "number" in (entry := row["data"]) - and entry["number"].get("info") != ImportState.WARNING - ], - field="number", - mapped_fields=["submitter_ids", "category_id", "block_id"], - global_and_filter=FilterOperator("meeting_id", "=", meeting_id), - ) - self.block_lookup = self.get_lookup_dict( - "motion_block", - [ - entry["block"]["value"] - for row in rows - if "block" in (entry := row["data"]) - and entry["block"].get("info") != ImportState.WARNING - ], - "title", - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - self.category_lookup = self.get_lookup_dict( - "motion_category", - [ - entry["category_name"]["value"] - for row in rows - if "category_name" in (entry := row["data"]) - and entry["category_name"].get("info") != ImportState.WARNING - ], - "name", - ["prefix"], - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - - self.username_lookup = self.get_lookup_dict( - "user", - list( - { - user["value"] - for row in rows - if ( - users := [ - *row["data"].get("submitters_username", []), - *row["data"].get("supporters_username", []), - ] - ) - for user in users - if user and user.get("info") != ImportState.WARNING - } - ), - "username", - ["meeting_ids", "meeting_user_ids"], - ) - self.username_lookup = { - username: [ - date - for date in self.username_lookup[username] - if ( - date.get("meeting_ids") - and (meeting_id in date["meeting_ids"]) - or date.get("id") == self.user_id - ) - ] - for username in self.username_lookup - } - all_user_ids = list( - [ - submitter["id"] - for submitters in self.username_lookup.values() - for submitter in submitters - ] - ) - all_meeting_users: dict[int, dict[str, Any]] = {} - if len(all_user_ids): - all_meeting_users = self.datastore.filter( - "meeting_user", - And( - FilterOperator("meeting_id", "=", meeting_id), - Or( - *[ - FilterOperator("user_id", "=", user_id) - for user_id in all_user_ids - ] - ), - ), - [ - "id", - "user_id", - "motion_submitter_ids", - "supported_motion_ids", - ], - lock_result=False, - ) - self._user_ids_to_meeting_user = { - all_meeting_users[meeting_user_id]["user_id"]: all_meeting_users[ - meeting_user_id - ] - for meeting_user_id in all_meeting_users - if all_meeting_users[meeting_user_id].get("user_id") - } - self._submitter_ids_to_user_id = { - submitter_id: all_meeting_users[meeting_user_id]["user_id"] - for meeting_user_id in all_meeting_users - for submitter_id in ( - all_meeting_users[meeting_user_id].get("motion_submitter_ids", []) or [] - ) - if all_meeting_users[meeting_user_id].get("user_id") - } - self.tags_lookup = self.get_lookup_dict( - "tag", - [ - tag["value"] - for row in rows - if "tags" in (entry := row["data"]) - for tag in entry["tags"] - if tag.get("info") != ImportState.WARNING - ], - "name", - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - - def get_lookup_dict( - self, - collection: str, - entries: list[str], - fieldname: str = "name", - mapped_fields: list[str] = [], - and_filters: list[Filter] = [], - ) -> dict[str, list[dict[str, Any]]]: - lookup: dict[str, list[dict[str, Any]]] = {} - if len(entries): - data = self.datastore.filter( - collection, - And( - *and_filters, - Or([FilterOperator(fieldname, "=", name) for name in set(entries)]), - ), - [*mapped_fields, "id", fieldname], - lock_result=False, - ) - for date_id in data: - date = data[date_id] - lookup[date[fieldname]] = [ - *lookup.get(date[fieldname], []), - date, - ] - return lookup - - def get_meeting_id(self, instance: dict[str, Any]) -> int: - store_id = instance["id"] - worker = self.datastore.get( - fqid_from_collection_and_id("import_preview", store_id), - ["name", "result"], - lock_result=False, - ) - if worker.get("name") == self.import_name: - return next(iter(worker.get("result", {})["rows"]))["data"]["meeting_id"] - raise ActionException("Import data cannot be found.") diff --git a/openslides_backend/action/actions/motion/json_upload.py b/openslides_backend/action/actions/motion/json_upload.py deleted file mode 100644 index fcb5741a4..000000000 --- a/openslides_backend/action/actions/motion/json_upload.py +++ /dev/null @@ -1,599 +0,0 @@ -from collections import defaultdict -from collections.abc import Iterable -from re import search, sub -from typing import Any, cast - -from openslides_backend.shared.filters import And, Filter, FilterOperator, Or - -from ....models.models import Motion -from ....permissions.permissions import Permissions -from ....shared.exceptions import ActionException -from ....shared.schema import required_id_schema -from ...mixins.import_mixins import ( - BaseJsonUploadAction, - ImportState, - Lookup, - ResultType, -) -from ...util.default_schema import DefaultSchema -from ...util.register import register_action -from .payload_validation_mixin import ( - MotionActionErrorData, - MotionCreatePayloadValidationMixin, - MotionErrorType, - MotionUpdatePayloadValidationMixin, -) - -LIST_TYPE = { - "anyOf": [ - { - "type": "array", - "items": {"type": "string"}, - }, - {"type": "string"}, - ] -} - - -@register_action("motion.json_upload") -class MotionJsonUpload( - BaseJsonUploadAction, - MotionCreatePayloadValidationMixin, - MotionUpdatePayloadValidationMixin, -): - """ - Action to allow to upload a json. It is used as first step of an import. - """ - - model = Motion() - schema = DefaultSchema(Motion()).get_default_schema( - additional_required_fields={ - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - **model.get_properties( - "title", - "text", - "number", - "reason", - ), - **{ - "submitters_verbose": LIST_TYPE, - "submitters_username": LIST_TYPE, - "supporters_verbose": LIST_TYPE, - "supporters_username": LIST_TYPE, - "category_name": {"type": "string"}, - "category_prefix": {"type": "string"}, - "tags": LIST_TYPE, - "block": {"type": "string"}, - "motion_amendment": {"type": "boolean"}, - }, - }, - "required": [], - "additionalProperties": False, - }, - "minItems": 1, - "uniqueItems": False, - }, - "meeting_id": required_id_schema, - } - ) - - headers = [ - {"property": "title", "type": "string", "is_object": True}, - {"property": "text", "type": "string", "is_object": True}, - {"property": "number", "type": "string", "is_object": True}, - {"property": "reason", "type": "string", "is_object": True}, - { - "property": "submitters_verbose", - "type": "string", - "is_list": True, - "is_hidden": True, - }, - { - "property": "submitters_username", - "type": "string", - "is_object": True, - "is_list": True, - }, - { - "property": "supporters_verbose", - "type": "string", - "is_list": True, - "is_hidden": True, - }, - { - "property": "supporters_username", - "type": "string", - "is_object": True, - "is_list": True, - }, - {"property": "category_name", "type": "string", "is_object": True}, - {"property": "category_prefix", "type": "string"}, - {"property": "tags", "type": "string", "is_object": True, "is_list": True}, - {"property": "block", "type": "string", "is_object": True}, - { - "property": "motion_amendment", - "type": "boolean", - "is_object": True, - "is_hidden": True, - }, - ] - permission = Permissions.Motion.CAN_MANAGE - row_state: ImportState - number_lookup: Lookup - username_lookup: dict[str, list[dict[str, Any]]] = {} - category_lookup: dict[str, list[dict[str, Any]]] = {} - tags_lookup: dict[str, list[dict[str, Any]]] = {} - block_lookup: dict[str, list[dict[str, Any]]] = {} - _first_state_id: int | None = None - _operator_username: str | None = None - _previous_numbers: list[str] - import_name = "motion" - - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: - # transform instance into a correct create/update payload - # try to find a pre-existing motion with the same number - # if there is one, validate for a motion.update, otherwise for a motion.create - # using get_update_payload_integrity_error_message and get_create_payload_integrity_error_message - - data = instance.pop("data") - data = self.add_payload_index_to_action_data(data) - self.setup_lookups(data, instance["meeting_id"]) - - # enrich data with meeting_id - for entry in data: - entry["meeting_id"] = instance["meeting_id"] - - self._previous_numbers = [] - self.rows = [self.validate_entry(entry) for entry in data] - - # generate statistics - self.generate_statistics() - return {} - - def validate_entry(self, entry: dict[str, Any]) -> dict[str, Any]: - messages: list[str] = [] - id_: int | None = None - meeting_id: int = entry["meeting_id"] - set_entry_id = False - - if (is_amendment := entry.get("motion_amendment")) is not None: - entry["motion_amendment"] = { - "value": is_amendment, - "info": ImportState.DONE, - } - if is_amendment: - entry["motion_amendment"]["info"] = ImportState.WARNING - messages.append("Amendments cannot be correctly imported") - - if category_name := entry.get("category_name"): - category_prefix = entry.get("category_prefix") - categories = self.category_lookup.get(category_name, []) - categories = [ - category - for category in categories - if category.get("prefix") == category_prefix - ] - if len(categories) == 1 and categories[0].get("id") != 0: - entry["category_name"] = { - "value": category_name, - "info": ImportState.DONE, - "id": categories[0].get("id"), - } - else: - entry["category_name"] = { - "value": category_name, - "info": ImportState.WARNING, - } - messages.append("Category could not be found") - elif category_prefix := entry.get("category_prefix"): - entry["category_name"] = {"value": "", "info": ImportState.WARNING} - messages.append("Category could not be found") - - if number := entry.get("number"): - check_result = self.number_lookup.check_duplicate(number) - id_ = cast(int, self.number_lookup.get_field_by_name(number, "id")) - if check_result == ResultType.FOUND_ID and id_ != 0: - self.row_state = ImportState.DONE - set_entry_id = True - entry["number"] = { - "value": number, - "info": ImportState.DONE, - "id": id_, - } - elif check_result == ResultType.NOT_FOUND or id_ == 0: - self.row_state = ImportState.NEW - entry["number"] = { - "value": number, - "info": ImportState.DONE, - } - elif check_result == ResultType.FOUND_MORE_IDS: - self.row_state = ImportState.ERROR - entry["number"] = { - "value": number, - "info": ImportState.ERROR, - } - messages.append("Error: Found multiple motions with the same number") - else: - category_id: int | None = None - if entry.get("category_name"): - category_id = entry["category_name"].get("id") - self.row_state = ImportState.NEW - value: dict[str, Any] = {} - self.set_number( - value, - meeting_id, - self._get_first_workflow_state_id(meeting_id), - None, - category_id, - other_forbidden_numbers=self._previous_numbers, - ) - if number := value.get("number"): - entry["number"] = {"value": number, "info": ImportState.GENERATED} - self._previous_numbers.append(number) - - has_submitter_error: bool = False - for fieldname in ["submitter", "supporter"]: - if users := entry.get(f"{fieldname}s_username"): - verbose = entry.get(f"{fieldname}s_verbose", []) - verbose_user_mismatch = len(verbose) > len(users) - username_set: set[str] = set() - entry_list: list[dict[str, Any]] = [] - duplicates: set[str] = set() - not_found: set[str] = set() - for user in users: - if verbose_user_mismatch: - entry_list.append({"value": user, "info": ImportState.ERROR}) - elif user in username_set: - entry_list.append({"value": user, "info": ImportState.WARNING}) - duplicates.add(user) - else: - username_set.add(user) - found_users = self.username_lookup.get(user, []) - if len(found_users) == 1 and found_users[0].get("id") != 0: - user_id = cast(int, found_users[0].get("id")) - entry_list.append( - { - "value": user, - "info": ImportState.DONE, - "id": user_id, - } - ) - elif len(found_users) <= 1: - entry_list.append( - { - "value": user, - "info": ImportState.WARNING, - } - ) - not_found.add(user) - else: - raise ActionException( - f"Database corrupt: Found multiple users with the username {user}" - ) - entry[f"{fieldname}s_username"] = entry_list - if verbose_user_mismatch: - self.row_state = ImportState.ERROR - messages.append( - f"Error: Verbose field is set and has more entries than the username field for {fieldname}s" - ) - if fieldname == "submitter": - has_submitter_error = True - if len(duplicates): - messages.append( - f"At least one {fieldname} has been referenced multiple times: " - + ", ".join(duplicates) - ) - if len(not_found): - messages.append( - f"Could not find at least one {fieldname}: " - + ", ".join(not_found) - ) - - if not has_submitter_error: - if ( - len(cast(list[dict[str, Any]], entry.get("submitters_username", []))) - == 0 - ): - entry["submitters_username"] = [self._get_self_username_object()] - elif ( - len( - [ - entry - for entry in ( - cast( - list[dict[str, Any]], - entry.get("submitters_username", []), - ) - ) - if entry.get("info") and (entry["info"] != ImportState.WARNING) - ] - ) - == 0 - ): - entry["submitters_username"].append(self._get_self_username_object()) - - if tags := entry.get("tags"): - entry_list = [] - duplicates = set() - not_found = set() - multiple: set[str] = set() - tags_set: set[str] = set() - for tag in tags: - if tag in tags_set: - entry_list.append({"value": tag, "info": ImportState.WARNING}) - duplicates.add(tag) - else: - tags_set.add(tag) - found_tags = self.tags_lookup.get(tag, []) - if len(found_tags) == 1 and found_tags[0].get("id") != 0: - tag_id = cast(int, found_tags[0].get("id")) - entry_list.append( - { - "value": tag, - "info": ImportState.DONE, - "id": tag_id, - } - ) - elif len(found_tags) <= 1: - entry_list.append( - { - "value": tag, - "info": ImportState.WARNING, - } - ) - not_found.add(tag) - else: - entry_list.append( - { - "value": tag, - "info": ImportState.WARNING, - } - ) - multiple.add(tag) - entry["tags"] = entry_list - if len(duplicates): - messages.append( - "At least one tag has been referenced multiple times: " - + ", ".join(duplicates) - ) - if len(not_found): - messages.append( - "Could not find at least one tag: " + ", ".join(not_found) - ) - if len(multiple): - messages.append( - "Found multiple tags with the same name: " + ", ".join(multiple) - ) - - if (block := entry.get("block")) and isinstance(block, str): - found_blocks = self.block_lookup.get(block, []) - if len(found_blocks) == 1 and found_blocks[0].get("id") != 0: - block_id = cast(int, found_blocks[0].get("id")) - entry["block"] = { - "value": block, - "info": ImportState.DONE, - "id": block_id, - } - elif len(found_blocks) <= 1: - entry["block"] = { - "value": block, - "info": ImportState.WARNING, - } - messages.append("Could not find motion block") - else: - entry["block"] = { - "value": block, - "info": ImportState.WARNING, - } - messages.append("Found multiple motion blocks with the same name") - - if id_ and set_entry_id: - entry["id"] = id_ - - if (text := entry.get("text")) and not search( - r"^<\w+[^>]*>[\w\W]*?<\/\w>$", text - ): - entry["text"] = ( - "

" - + sub(r"\n", "
", sub(r"\n([ \t]*\n)+", "

", text)) - + "

" - ) - - for field in ["title", "text", "reason"]: - if (date := entry.get(field)) and isinstance(date, str): - if date == "": - del entry[field] - else: - entry[field] = {"value": date, "info": ImportState.DONE} - - # check via mixin - payload = { - **{ - k: v.get("value") - for k, v in entry.items() - if k in ["title", "text", "number", "reason"] - }, - **{ - k: self._get_field_ids(entry, v) - for k, v in { - "submitter_ids": "submitters_username", - "supporter_meeting_user_ids": "supporters_username", - "tag_ids": "tags", - }.items() - }, - **{ - k: self._get_field_id(entry, v) - for k, v in { - "category_id": "category_name", - "block_id": "block", - }.items() - if entry.get(v) - }, - } - - errors: list[MotionActionErrorData] = [] - if id_: - payload = {"id": id_, **payload} - errors = self.get_update_payload_integrity_error_message( - payload, meeting_id - ) - else: - payload = {"meeting_id": meeting_id, **payload} - errors = self.get_create_payload_integrity_error_message( - payload, meeting_id - ) - - for err in errors: - entry = self._add_error_to_entry(entry, err) - messages.append("Error: " + err["message"]) - - return {"state": self.row_state, "messages": messages, "data": entry} - - def setup_lookups(self, data: Iterable[dict[str, Any]], meeting_id: int) -> None: - self.number_lookup = Lookup( - self.datastore, - "motion", - [(number, entry) for entry in data if (number := entry.get("number"))], - field="number", - mapped_fields=[], - global_and_filter=FilterOperator("meeting_id", "=", meeting_id), - ) - self.block_lookup = self.get_lookup_dict( - "motion_block", - [title for entry in data if (title := entry.get("block"))], - "title", - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - self.category_lookup = self.get_lookup_dict( - "motion_category", - [name for entry in data if (name := entry.get("category_name"))], - "name", - ["prefix"], - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - self.username_lookup = self.get_lookup_dict( - "user", - list( - { - username - for entry in data - if ( - usernames := [ - *entry.get("submitters_username", []), - *entry.get("supporters_username", []), - ] - ) - for username in usernames - if username - } - ), - "username", - ["meeting_ids"], - ) - self.username_lookup = { - username: [ - date - for date in self.username_lookup[username] - if date.get("meeting_ids") and (meeting_id in date["meeting_ids"]) - ] - for username in self.username_lookup - } - self.tags_lookup = self.get_lookup_dict( - "tag", - [ - name - for entry in data - if (names := entry.get("tags")) - for name in names - if name - ], - "name", - and_filters=[FilterOperator("meeting_id", "=", meeting_id)], - ) - - def get_lookup_dict( - self, - collection: str, - entries: list[str], - fieldname: str = "name", - mapped_fields: list[str] = [], - and_filters: list[Filter] = [], - ) -> dict[str, list[dict[str, Any]]]: - lookup: dict[str, list[dict[str, Any]]] = defaultdict(list) - if len(entries): - data = self.datastore.filter( - collection, - And( - *and_filters, - Or([FilterOperator(fieldname, "=", name) for name in set(entries)]), - ), - [*mapped_fields, "id", fieldname], - lock_result=False, - ) - for date in data.values(): - lookup[date[fieldname]].append(date) - return lookup - - def _get_self_username_object(self) -> dict[str, Any]: - if not self._operator_username: - user = self.datastore.get("user/" + str(self.user_id), ["username"]) - if not (user and user.get("username")): - raise ActionException("Couldn't find operator's username") - self._operator_username = cast(str, user["username"]) - return { - "value": self._operator_username, - "info": ImportState.GENERATED, - "id": self.user_id, - } - - def _get_first_workflow_state_id(self, meeting_id: int) -> int: - if not self._first_state_id: - default_workflows = self.datastore.filter( - "motion_workflow", - FilterOperator("default_workflow_meeting_id", "=", meeting_id), - mapped_fields=["first_state_id"], - ).values() - if len(default_workflows) != 1: - raise ActionException("Couldn't determine default workflow") - self._first_state_id = cast( - int, list(default_workflows)[0].get("first_state_id") - ) - return self._first_state_id - - def _get_field_ids(self, entry: dict[str, Any], fieldname: str) -> list[int]: - value = entry.get(fieldname, []) - if not isinstance(value, list): - value = [entry[fieldname]] - return [val["id"] for val in value if val.get("id")] - - def _get_field_id(self, entry: dict[str, Any], fieldname: str) -> int: - return entry[fieldname].get("id") - - def _add_error_to_entry( - self, entry: dict[str, Any], err: MotionActionErrorData - ) -> dict[str, Any]: - fieldname = "" - match err["type"]: - case MotionErrorType.UNIQUE_NUMBER: - fieldname = "number" - case MotionErrorType.TEXT: - fieldname = "text" - case MotionErrorType.REASON: - fieldname = "reason" - case MotionErrorType.TITLE: - fieldname = "title" - case _: - raise ActionException("Error: " + err["message"]) - if not (entry.get(fieldname) and isinstance(entry[fieldname], dict)): - entry[fieldname] = { - "value": entry.get(fieldname, ""), - "info": ImportState.ERROR, - } - else: - entry[fieldname]["info"] = ImportState.ERROR - self.row_state = ImportState.ERROR - return entry diff --git a/tests/system/action/motion/test_import.py b/tests/system/action/motion/test_import.py deleted file mode 100644 index da996334e..000000000 --- a/tests/system/action/motion/test_import.py +++ /dev/null @@ -1,1161 +0,0 @@ -from openslides_backend.action.mixins.import_mixins import ImportState - -from .test_json_upload import MotionJsonUploadForUseInImport - - -class MotionImport(MotionJsonUploadForUseInImport): - def test_import_database_corrupt(self) -> None: - self.create_meeting(42) - self.set_models( - { - "import_preview/2": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "state": (ImportState.DONE), - "messages": [], - "data": { - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "reason": {"value": "", "info": ImportState.DONE}, - "supporters_username": [], - "category_name": { - "value": "", - "info": ImportState.DONE, - }, - "tags": [], - "block": {"value": "", "info": ImportState.DONE}, - "number": { - "info": ImportState.DONE, - "id": 101, - "value": "NUM01", - }, - "submitters_username": [ - { - "info": ImportState.DONE, - "id": 12345678, - "value": "bob", - } - ], - "id": 101, - "meeting_id": 42, - }, - } - ], - }, - }, - "user/12345678": { - "username": "bob", - "meeting_ids": [42], - "meeting_user_ids": [12345678], - }, - "meeting_user/12345678": {}, - "user/123456789": { - "username": "bob", - "meeting_ids": [42], - "meeting_user_ids": [123456789], - }, - "meeting_user/123456789": {}, - "meeting/42": {"meeting_user_ids": [12345678, 123456789]}, - } - ) - response = self.request("motion.import", {"id": 2, "import": True}) - self.assert_status_code(response, 400) - assert ( - "Database corrupt: Found multiple users with the username bob." - in response.json["message"] - ) - - def test_import_abort(self) -> None: - self.create_meeting(42) - self.set_models( - { - "import_preview/2": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "number": {"value": "", "info": ImportState.DONE}, - "reason": {"value": "", "info": ImportState.DONE}, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "supporters_username": [], - "category_name": { - "value": "", - "info": ImportState.DONE, - }, - "tags": [], - "block": {"value": "", "info": ImportState.DONE}, - "meeting_id": 42, - }, - } - ], - }, - }, - } - ) - response = self.request("motion.import", {"id": 2, "import": False}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("import_preview/2") - self.assert_model_not_exists("motion/1") - - def test_import_wrong_import_preview(self) -> None: - self.create_meeting(42) - self.set_models( - { - "import_preview/3": {"result": None}, - "import_preview/4": { - "state": ImportState.DONE, - "name": "topic", - "result": { - "rows": [ - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": {"value": "test", "info": ImportState.NEW}, - "meeting_id": 42, - }, - }, - ], - }, - }, - } - ) - response = self.request("motion.import", {"id": 3, "import": True}) - self.assert_status_code(response, 400) - assert ( - "Wrong id doesn't point on motion import data." in response.json["message"] - ) - - def test_import_non_existant_ids(self) -> None: - self.create_meeting(42) - preview_rows = [ - { - "state": ImportState.DONE, - "messages": [], - "data": { - "id": 1, - "title": { - "value": "Update", - "info": ImportState.DONE, - }, - "text": { - "value": "

of non-existant motion", - "info": ImportState.DONE, - }, - "number": { - "id": 1, - "value": "NOMNOMNOM1", - "info": ImportState.DONE, - }, - "submitters_username": [ - { - "value": "nonExistantUser", - "info": ImportState.DONE, - "id": 2, - } - ], - "supporters_username": [ - { - "value": "NoOne", - "info": ImportState.DONE, - "id": 3, - } - ], - "category_name": { - "id": 8, - "value": "NonCategory", - "info": ImportState.DONE, - }, - "tags": [ - { - "id": 9, - "value": "NonTag", - "info": ImportState.DONE, - } - ], - "block": {"id": 9, "value": "NonBlock", "info": ImportState.DONE}, - "meeting_id": 42, - }, - }, - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": { - "value": "Create", - "info": ImportState.DONE, - }, - "text": { - "value": "

a new motion

", - "info": ImportState.DONE, - }, - "number": {"value": "NEW", "info": ImportState.DONE}, - "submitters_username": [ - { - "value": "nonUser", - "info": ImportState.DONE, - "id": 4, - } - ], - "supporters_username": [ - { - "value": "NoOne", - "info": ImportState.DONE, - "id": 5, - } - ], - "category_name": { - "id": 10, - "value": "NonCategoryTwoElectricBoogaloo", - "info": ImportState.DONE, - }, - "tags": [ - { - "id": 11, - "value": "TagNot", - "info": ImportState.DONE, - } - ], - "block": { - "id": 12, - "value": "JustBlockIt", - "info": ImportState.DONE, - }, - "meeting_id": 42, - }, - }, - ] - self.set_models( - { - "import_preview/2": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": preview_rows, - }, - }, - } - ) - response = self.request("motion.import", {"id": 2, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_not_exists("motion/1") - self.assert_model_not_exists("motion/2") - meeting = self.assert_model_exists("meeting/42") - assert "motion_ids" not in meeting - response_rows = response.json["results"][0][0]["rows"] - assert response_rows[0]["data"] == { - "id": 1, - "tags": [{"info": "error", "value": "NonTag"}], - "text": {"info": "done", "value": "

of non-existant motion"}, - "block": {"value": "NonBlock", "info": "error"}, - "title": {"info": "done", "value": "Update"}, - "number": {"id": 1, "info": "error", "value": "NOMNOMNOM1"}, - "meeting_id": 42, - "category_name": {"value": "NonCategory", "info": "error"}, - "submitters_username": [{"info": "error", "value": "nonExistantUser"}], - "supporters_username": [{"info": "error", "value": "NoOne"}], - } - assert sorted(response_rows[0]["messages"]) == sorted( - [ - "Error: Couldn't find motion block anymore", - "Error: Couldn't find supporter anymore: NoOne", - "Error: Couldn't find tag anymore: NonTag", - "Error: Category could not be found anymore", - "Error: Couldn't find submitter anymore: nonExistantUser", - "Error: Motion 1 not found anymore for updating motion 'NOMNOMNOM1'.", - ] - ) - assert response_rows[1]["data"] == { - "tags": [{"info": "error", "value": "TagNot"}], - "text": {"info": "done", "value": "

a new motion

"}, - "block": {"value": "JustBlockIt", "info": "error"}, - "title": {"info": "done", "value": "Create"}, - "number": {"info": "done", "value": "NEW"}, - "meeting_id": 42, - "category_name": { - "value": "NonCategoryTwoElectricBoogaloo", - "info": "error", - }, - "submitters_username": [{"info": "error", "value": "nonUser"}], - "supporters_username": [{"info": "error", "value": "NoOne"}], - } - assert sorted(response_rows[1]["messages"]) == sorted( - [ - "Error: Couldn't find motion block anymore", - "Error: Couldn't find supporter anymore: NoOne", - "Error: Couldn't find tag anymore: TagNot", - "Error: Category could not be found anymore", - "Error: Couldn't find submitter anymore: nonUser", - ] - ) - - def test_import_with_deleted_references(self) -> None: - self.json_upload_multi_row() - self.request("motion.delete", {"id": 100}) - self.request("user.delete", {"id": 2}) - self.request("meeting_user.delete", {"id": 3}) - self.request("motion_category.delete", {"id": 100}) - self.request("motion_category.delete", {"id": 1000}) - self.request("motion_block.delete", {"id": 1}) - self.request("tag.delete", {"id": 1}) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/1", - { - "title": "A", - "text": "

nice little

", - "reason": "motion", - }, - ) - self.assert_model_exists("meeting/1", {"motion_ids": [1]}) - self.assert_model_deleted("motion/100") - self.assert_model_not_exists("motion/101") - self.assert_model_not_exists("motion/102") - self.assert_model_not_exists("motion/103") - rows = response.json["results"][0][0]["rows"] - assert len(rows) == 5 - row = rows[0] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Couldn't find supporter anymore: user1"] - assert row["data"]["supporters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"} - ] - row = rows[1] - assert row["state"] == ImportState.ERROR - assert sorted(row["messages"]) == sorted( - [ - "Error: Motion 100 not found anymore for updating motion 'NUM02'.", - "Error: Couldn't find submitter anymore: user1", - "Error: Category could not be found anymore", - ] - ) - assert row["data"]["number"] == { - "id": 100, - "value": "NUM02", - "info": ImportState.ERROR, - } - assert row["data"]["category_name"] == { - "info": ImportState.ERROR, - "value": "Other motion", - } - assert row["data"]["submitters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"} - ] - row = rows[2] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Couldn't find tag anymore: Tag1"] - assert row["data"]["tags"] == [{"info": ImportState.ERROR, "value": "Tag1"}] - row = rows[3] - assert row["state"] == ImportState.ERROR - assert sorted(row["messages"]) == sorted( - [ - "Error: Couldn't find supporter anymore: user1, anotherUser", - "Error: Couldn't find motion block anymore", - ] - ) - assert row["data"]["supporters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"}, - {"id": 1, "info": ImportState.DONE, "value": "admin"}, - {"info": ImportState.ERROR, "value": "anotherUser"}, - ] - assert row["data"]["block"] == {"info": ImportState.ERROR, "value": "Block1"} - row = rows[4] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Category could not be found anymore"] - assert row["data"]["category_name"] == { - "info": ImportState.ERROR, - "value": "Other motion", - } - - def test_import_with_changed_references(self) -> None: - self.json_upload_multi_row() - self.set_models( - { - "user/2": {"username": "changedName"}, - "user/3": {"username": "changedNameToo"}, - "motion_category/100": {"name": "changedName"}, - "motion_category/1000": {"prefix": "changedPREFIX"}, - "motion_block/1": {"title": "changedTitle"}, - "tag/1": {"name": "changedName"}, - } - ) - self.create_user("anotherUser", [1]) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - rows = response.json["results"][0][0]["rows"] - assert len(rows) == 5 - row = rows[0] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Couldn't find supporter anymore: user1"] - assert row["data"]["supporters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"} - ] - row = rows[1] - assert row["state"] == ImportState.ERROR - assert sorted(row["messages"]) == sorted( - [ - "Error: Couldn't find submitter anymore: user1", - "Error: Category could not be found anymore", - ] - ) - assert row["data"]["category_name"] == { - "info": ImportState.ERROR, - "value": "Other motion", - } - assert row["data"]["submitters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"} - ] - row = rows[2] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Couldn't find tag anymore: Tag1"] - assert row["data"]["tags"] == [{"info": ImportState.ERROR, "value": "Tag1"}] - row = rows[3] - assert row["state"] == ImportState.ERROR - assert sorted(row["messages"]) == sorted( - [ - "Error: Supporter search didn't deliver the same result as in the preview: anotherUser", - "Error: Couldn't find motion block anymore", - "Error: Couldn't find supporter anymore: user1", - ] - ) - assert row["data"]["supporters_username"] == [ - {"info": ImportState.ERROR, "value": "user1"}, - {"id": 1, "info": ImportState.DONE, "value": "admin"}, - {"info": ImportState.ERROR, "value": "anotherUser"}, - ] - assert row["data"]["block"] == {"info": ImportState.ERROR, "value": "Block1"} - row = rows[4] - assert row["state"] == ImportState.ERROR - assert row["messages"] == ["Error: Category could not be found anymore"] - assert row["data"]["category_name"] == { - "info": ImportState.ERROR, - "value": "Other motion", - } - - def test_import_wrong_meeting_model_import_preview(self) -> None: - self.create_meeting(42) - self.set_models( - { - "import_preview/4": { - "state": ImportState.DONE, - "name": "topic", - "result": { - "rows": [ - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": {"value": "test", "info": ImportState.NEW}, - "meeting_id": 42, - }, - }, - ], - }, - }, - } - ) - response = self.request("motion.import", {"id": 4, "import": True}) - self.assert_status_code(response, 400) - assert ( - "Wrong id doesn't point on motion import data." in response.json["message"] - ) - - def test_json_upload_amendment(self) -> None: - self.json_upload_amendment() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.DONE - - def test_json_upload_with_errors(self) -> None: - self.json_upload_create_missing_title() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 400) - assert "Error in import. Data will not be imported." in response.json["message"] - - def test_json_upload_update_missing_title(self) -> None: - self.json_upload_update_missing_title() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.DONE - self.assert_model_exists("motion/42", {"title": "A"}) - - def test_json_upload_update_missing_reason_although_required(self) -> None: - self.json_upload_update_missing_reason_although_required() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.DONE - self.assert_model_exists("motion/42", {"reason": "motion"}) - - def test_json_upload_multi_row(self) -> None: - self.json_upload_multi_row() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/1", - { - "title": "first", - "text": "

test my stuff

", - "reason": "motion", # retained from before - "supporter_meeting_user_ids": [1], - "submitter_ids": [4], - }, - ) - self.assert_model_exists("motion_submitter/4", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/1", {"user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - self.assert_model_exists( - "motion/100", - { - "title": "then", - "text": "

nice little

", # retained from before - "reason": "test my other stuff", - "submitter_ids": [5], - "category_id": 1000, - }, - ) - self.assert_model_exists("motion_submitter/5", {"meeting_user_id": 1}) - self.assert_model_exists( - "motion/101", - { - "number": "NUM03", - "title": "also", - "text": "

test the other peoples stuff

", - "submitter_ids": [1], - "tag_ids": [1], - }, - ) - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists( - "motion/102", - { - "number": "03", - "title": "after that", - "text": "

test even more stuff

", - "submitter_ids": [2], - "supporter_meeting_user_ids": [1, 2, 3], - "block_id": 1, - }, - ) - self.assert_model_exists("motion_submitter/2", {"meeting_user_id": 2}) - self.assert_model_exists( - "motion/103", - { - "number": "OTHER01", - "title": "finally", - "text": "

finish testing

", - "submitter_ids": [3], - "category_id": 100, - }, - ) - self.assert_model_exists("motion_submitter/3", {"meeting_user_id": 2}) - - def test_simple_create(self) -> None: - self.json_upload_simple_create() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - motion = self.assert_model_exists( - "motion/4201", - { - "title": "test", - "text": "

my

", - "reason": "stuff", - "submitter_ids": [1], - }, - ) - assert "number" not in motion - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - - def test_simple_create_with_reason_required(self) -> None: - self.json_upload_simple_create(True) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - motion = self.assert_model_exists( - "motion/4201", - { - "title": "test", - "text": "

my

", - "reason": "stuff", - "submitter_ids": [1], - }, - ) - assert "number" not in motion - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - - def test_simple_create_with_set_number(self) -> None: - self.json_upload_simple_create(is_set_number=True) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/4201", - { - "number": "03", - "title": "test", - "text": "

my

", - "reason": "stuff", - "submitter_ids": [1], - }, - ) - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - - def test_simple_update(self) -> None: - self.json_upload_simple_update() - self.assert_simple_update_successful() - - def test_simple_update_with_reason_required(self) -> None: - self.json_upload_simple_update(True) - self.assert_simple_update_successful() - - def test_simple_update_with_set_number(self) -> None: - self.json_upload_simple_update(is_set_number=True) - self.assert_simple_update_successful() - - def assert_simple_update_successful(self) -> None: - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/42", - { - "number": "NUM01", - "title": "test", - "text": "

my

", - "reason": "stuff", - "submitter_ids": [1], - }, - ) - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - - def test_json_upload_update_with_foreign_meeting(self) -> None: - self.json_upload_update_with_foreign_meeting() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/42", - { - "number": "NUM01", - "title": "test", - "text": "

my

", - "reason": "stuff", - "category_id": 42, - "submitter_ids": [1], - "supporter_meeting_user_ids": [1], - }, - ) - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 3}) - self.assert_model_exists("meeting_user/3", {"user_id": 1}) - - def test_json_upload_custom_number_create(self) -> None: - self.json_upload_custom_number_create() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/4201", - { - "number": "Z01", - "title": "test", - "text": "

my

", - "reason": "stuff", - "category_id": 420, - }, - ) - - def test_json_upload_custom_number_create_with_set_number(self) -> None: - self.json_upload_custom_number_create_with_set_number() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/4201", - { - "number": "Z01", - "title": "test", - "text": "

my

", - "reason": "stuff", - "category_id": 420, - }, - ) - - def test_with_warnings(self) -> None: - self.json_upload_with_warnings() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - motion = self.assert_model_exists("motion/10") - assert "category_id" not in motion - assert motion["submitter_ids"] == [3] - self.assert_model_exists("motion_submitter/3", {"meeting_user_id": 1}) - self.assert_model_exists("meeting_user/1", {"user_id": 2}) - - motion = self.assert_model_exists("motion/1001") - assert "category_id" not in motion - assert motion["submitter_ids"] == [1] - self.assert_model_exists("motion_submitter/1", {"meeting_user_id": 2}) - self.assert_model_exists("meeting_user/2", {"user_id": 1}) - assert motion["supporter_meeting_user_ids"] == [1] - - motion = self.assert_model_exists("motion/1002") - assert "category_id" not in motion - assert motion["submitter_ids"] == [2] - self.assert_model_exists("motion_submitter/2", {"meeting_user_id": 2}) - assert len(motion["supporter_meeting_user_ids"]) == 0 - - def test_with_non_matching_verbose_users_okay(self) -> None: - self.json_upload_with_non_matching_verbose_users_okay() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/123", - { - "number": "NUM01", - "title": "Up", - "text": "

date

", - "submitter_ids": [2, 3], - "supporter_meeting_user_ids": [1, 2], - }, - ) - newMotion = self.assert_model_exists( - "motion/12301", - { - "title": "Newer", - "text": "

motion

", - "submitter_ids": [1], - }, - ) - assert len(newMotion.get("supporter_meeting_user_ids", [])) == 0 - self.assert_model_exists( - "meeting_user/1", {"user_id": 2, "motion_submitter_ids": [2]} - ) - self.assert_model_exists( - "meeting_user/2", {"user_id": 3, "motion_submitter_ids": [3]} - ) - self.assert_model_exists( - "meeting_user/3", {"user_id": 1, "motion_submitter_ids": [1]} - ) - - def test_with_tags_and_blocks(self) -> None: - self.json_upload_with_tags_and_blocks() - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/42", - { - "number": "NUM01", - "tag_ids": [1, 2], - "block_id": 1, - }, - ) - self.assert_model_exists("motion/5501", {"tag_ids": [1, 2], "block_id": 2}) - mot3 = self.assert_model_exists("motion/5502", {"tag_ids": []}) - assert "block_id" not in mot3 - mot4 = self.assert_model_exists("motion/5503", {"tag_ids": []}) - assert "block_id" not in mot4 - - def test_with_new_duplicate_tags_and_blocks(self) -> None: - self.json_upload_with_tags_and_blocks() - self.set_models( - { - "tag/7": {"name": "Tag-liatelle", "meeting_id": 42}, - "motion_block/7": {"title": "Blockolade", "meeting_id": 42}, - "meeting/42": { - "tag_ids": [1, 2, 3, 4, 7], - "motion_block_ids": [1, 2, 3, 4, 7], - }, - } - ) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "motion/42", - { - "number": "NUM01", - "tag_ids": [1, 2], - "block_id": 1, - }, - ) - self.assert_model_exists("motion/5501", {"tag_ids": [1, 2], "block_id": 2}) - mot3 = self.assert_model_exists("motion/5502", {"tag_ids": []}) - assert "block_id" not in mot3 - mot4 = self.assert_model_exists("motion/5503", {"tag_ids": []}) - assert "block_id" not in mot4 - - def test_update_with_changed_number(self) -> None: - self.json_upload_simple_update() - self.set_models({"motion/42": {"number": "CHANGED01"}}) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["data"]["number"] == { - "id": 42, - "info": ImportState.ERROR, - "value": "NUM01", - } - assert response.json["results"][0][0]["rows"][0]["messages"] == [ - "Error: Motion 42 not found anymore for updating motion 'NUM01'." - ] - - def test_import_with_newly_duplicate_number(self) -> None: - self.setup_meeting_with_settings(5, True, True) - self.set_models( - { - "import_preview/55": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "number": { - "info": ImportState.DONE, - "value": "NUM01", - }, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "meeting_id": 5, - }, - } - ], - }, - } - } - ) - response = self.request("motion.import", {"id": 55, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["messages"] == [ - "Error: Row state expected to be 'done', but it is 'new'." - ] - assert ( - response.json["results"][0][0]["rows"][0]["data"]["number"]["info"] - == ImportState.ERROR - ) - - def test_import_without_reason_when_required(self) -> None: - self.setup_meeting_with_settings(5, True, True) - self.set_models( - { - "import_preview/55": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "state": ImportState.NEW, - "messages": [], - "data": { - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "meeting_id": 5, - }, - } - ], - }, - } - } - ) - response = self.request("motion.import", {"id": 55, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["messages"] == [ - "Error: Reason is required" - ] - assert response.json["results"][0][0]["rows"][0]["data"]["reason"] == { - "value": "", - "info": ImportState.ERROR, - } - - def test_update_with_replaced_number(self) -> None: - self.json_upload_simple_update() - self.set_models( - { - "motion/42": {"number": "CHANGED01"}, - "motion/56": { - "meeting_id": 42, - "number": "NUM01", - "title": "Impostor", - "text": "

motion

", - }, - "meeting/42": {"motion_ids": [42, 56, 4200]}, - } - ) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["data"]["number"] == { - "id": 42, - "info": ImportState.ERROR, - "value": "NUM01", - } - assert response.json["results"][0][0]["rows"][0]["messages"] == [ - "Error: Number 'NUM01' found in different id (56 instead of 42)" - ] - - def test_update_with_duplicated_number(self) -> None: - self.json_upload_simple_update() - self.set_models( - { - "motion/56": { - "meeting_id": 42, - "number": "NUM01", - "title": "Impostor", - "text": "

motion

", - }, - "meeting/42": {"motion_ids": [42, 56, 4200]}, - } - ) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["data"]["number"] == { - "id": 42, - "info": ImportState.ERROR, - "value": "NUM01", - } - assert response.json["results"][0][0]["rows"][0]["messages"] == [ - "Error: Number 'NUM01' is duplicated in import." - ] - - def test_update_with_duplicated_number_2(self) -> None: - self.setup_meeting_with_settings(5, True, True) - row = { - "state": ImportState.DONE, - "messages": [], - "data": { - "id": 5, - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "number": {"value": "NUM01", "info": ImportState.DONE, "id": 5}, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "meeting_id": 5, - }, - } - self.set_models( - { - "import_preview/123": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [row, row.copy()], - }, - } - } - ) - response = self.request("motion.import", {"id": 123, "import": True}) - self.assert_status_code(response, 200) - for i in range(2): - assert ( - response.json["results"][0][0]["rows"][i]["state"] == ImportState.ERROR - ) - assert response.json["results"][0][0]["rows"][i]["data"]["number"] == { - "id": 5, - "info": ImportState.ERROR, - "value": "NUM01", - } - assert response.json["results"][0][0]["rows"][i]["messages"] == [ - "Error: Number 'NUM01' is duplicated in import." - ] - - def test_with_replaced_tags_and_blocks(self) -> None: - self.json_upload_with_tags_and_blocks() - self.set_models( - { - "tag/1": {"name": "Changed"}, - "tag/7": {"name": "Tag-liatelle", "meeting_id": 42}, - "motion_block/1": {"title": "Changed"}, - "motion_block/7": {"title": "Blockolade", "meeting_id": 42}, - "meeting/42": { - "tag_ids": [1, 2, 3, 4, 7], - "motion_block_ids": [1, 2, 3, 4, 7], - }, - } - ) - response = self.request("motion.import", {"id": 1, "import": True}) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.ERROR - assert response.json["results"][0][0]["rows"][0]["data"]["tags"] == [ - { - "info": ImportState.ERROR, - "value": "Tag-liatelle", - }, - {"id": 2, "info": ImportState.DONE, "value": "Tag-you're-it"}, - {"value": "Tag-ether", "info": "warning"}, - {"value": "Price tag", "info": "warning"}, - {"value": "Not a tag", "info": "warning"}, - ] - assert response.json["results"][0][0]["rows"][0]["data"]["block"] == { - "info": ImportState.ERROR, - "value": "Blockolade", - } - assert ( - "Error: Tag search didn't deliver the same result as in the preview: Tag-liatelle" - in response.json["results"][0][0]["rows"][0]["messages"] - ) - assert ( - "Error: Motion block search didn't deliver the same result as in the preview" - in response.json["results"][0][0]["rows"][0]["messages"] - ) - - def test_update_with_broken_id_entries(self) -> None: - self.setup_meeting_with_settings(5, True, True) - self.set_models( - { - "import_preview/123": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "id": 5, - "state": ImportState.DONE, - "messages": [], - "data": { - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "number": { - "value": "NUM01", - "info": ImportState.DONE, - "id": 5, - }, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "meeting_id": 5, - }, - }, - ], - }, - } - } - ) - response = self.request("motion.import", {"id": 123, "import": True}) - self.assert_status_code(response, 400) - assert ( - "Invalid JsonUpload data: A data row with state 'done' must have an 'id'" - in response.json["message"] - ) - - def test_update_with_broken_id_entries_2(self) -> None: - self.setup_meeting_with_settings(5, True, True) - self.set_models( - { - "import_preview/123": { - "state": ImportState.DONE, - "name": "motion", - "result": { - "rows": [ - { - "state": ImportState.DONE, - "messages": [], - "data": { - "id": 5, - "title": { - "value": "New", - "info": ImportState.DONE, - }, - "text": { - "value": "Motion", - "info": ImportState.DONE, - }, - "number": { - "value": "NUM01", - "info": ImportState.DONE, - }, - "submitters_username": [ - { - "value": "admin", - "info": ImportState.GENERATED, - "id": 1, - } - ], - "meeting_id": 5, - }, - }, - ], - }, - } - } - ) - response = self.request("motion.import", {"id": 123, "import": True}) - self.assert_status_code(response, 400) - assert ( - "Invalid JsonUpload data: A data row with state 'done' must have an 'id'" - in response.json["message"] - ) diff --git a/tests/system/action/motion/test_json_upload.py b/tests/system/action/motion/test_json_upload.py deleted file mode 100644 index 903708ae4..000000000 --- a/tests/system/action/motion/test_json_upload.py +++ /dev/null @@ -1,974 +0,0 @@ -from openslides_backend.action.mixins.import_mixins import ImportState -from tests.system.action.base import BaseActionTestCase - - -class BaseMotionJsonUpload(BaseActionTestCase): - def setup_meeting_with_settings( - self, - id_: int = 42, - is_reason_required: bool = False, - is_set_number: bool = False, - ) -> None: - self.create_meeting(id_) - self.create_user(f"user{id_}", [id_], None) - self.set_models( - { - f"meeting/{id_}": { - "motions_reason_required": is_reason_required, - "motions_number_type": "per_category", - "motions_number_min_digits": 2, - "motion_ids": [id_, id_ * 100], - "motion_category_ids": [id_, id_ * 10, id_ * 100, id_ * 1000], - }, - f"motion_state/{id_}": {"set_number": is_set_number}, - f"motion/{id_}": { - "title": "A", - "text": "

nice little

", - "reason": "motion", - "meeting_id": id_, - "number": "NUM01", - "number_value": 1, - }, - f"motion/{id_ * 100}": { - "title": "Another", - "text": "

nice little

", - "reason": "motion", - "meeting_id": id_, - "number": "NUM02", - "number_value": 2, - }, - f"motion_category/{id_}": { - "name": "Normal motion", - "prefix": "NORM", - "meeting_id": id_, - }, - f"motion_category/{id_ * 10}": { - "name": "Other motion", - "prefix": "NORM", - "meeting_id": id_, - }, - f"motion_category/{id_ * 100}": { - "name": "Other motion", - "prefix": "OTHER", - "meeting_id": id_, - }, - f"motion_category/{id_ * 1000}": { - "name": "Other motion", - "meeting_id": id_, - }, - } - ) - - -class MotionJsonUpload(BaseMotionJsonUpload): - def test_json_upload_empty_data(self) -> None: - response = self.request( - "motion.json_upload", - {"data": [], "meeting_id": 42}, - ) - self.assert_status_code(response, 400) - assert "data.data must contain at least 1 items" in response.json["message"] - - def test_json_upload_unknown_meeting_id(self) -> None: - self.setup_meeting_with_settings(42) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "reason": "stuff"}], - "meeting_id": 41, - }, - ) - self.assert_status_code(response, 400) - assert "Import tries to use non-existent meeting 41" in response.json["message"] - - def test_json_upload_create_missing_text(self) -> None: - self.setup_meeting_with_settings(42) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "reason": "stuff"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.ERROR - assert ( - "Error: Text is required" - in response.json["results"][0][0]["rows"][0]["messages"] - ) - assert response.json["results"][0][0]["rows"][0]["data"]["text"] == { - "value": "", - "info": ImportState.ERROR, - } - - def test_json_upload_create_missing_reason_although_required(self) -> None: - self.setup_meeting_with_settings(42, is_reason_required=True) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "text": "my"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.ERROR - assert ( - "Error: Reason is required" - in response.json["results"][0][0]["rows"][0]["messages"] - ) - assert response.json["results"][0][0]["rows"][0]["data"]["reason"] == { - "value": "", - "info": ImportState.ERROR, - } - - def assert_duplicate_numbers(self, number: str) -> None: - self.setup_meeting_with_settings(22) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "test", - "text": "my", - "reason": "stuff", - }, - { - "number": "NUM01", - "title": "test also", - "text": "

my other

", - "reason": "stuff", - }, - { - "number": "NUM04", - "title": "test", - "text": "my", - "reason": "stuff", - }, - { - "number": "NUM04", - "title": "test also", - "text": "

my other

", - "reason": "stuff", - }, - ], - "meeting_id": 22, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 4 - result = response.json["results"][0][0] - assert result["state"] == ImportState.ERROR - for i in range(4): - assert result["rows"][i]["state"] == ImportState.ERROR - assert ( - "Error: Found multiple motions with the same number" - in result["rows"][i]["messages"] - ) - assert result["rows"][i]["data"]["number"] == { - "value": number, - "info": ImportState.ERROR, - } - - def test_duplicate_numbers_in_datastore(self) -> None: - self.setup_meeting_with_settings(22) - self.set_models( - { - "motion/23": { - "meeting_id": 22, - "number": "NUM01", - "title": "Title", - "text": "

Text

", - }, - "meeting/22": {"motion_ids": [22, 23, 2200]}, - } - ) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "test", - "text": "my", - "reason": "stuff", - }, - ], - "meeting_id": 22, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 1 - result = response.json["results"][0][0] - assert result["state"] == ImportState.ERROR - assert result["rows"][0]["state"] == ImportState.ERROR - assert sorted(result["rows"][0]["messages"]) == sorted( - [ - "Error: Found multiple motions with the same number", - "Error: Number is not unique.", - ] - ) - assert result["rows"][0]["data"]["number"] == { - "value": "NUM01", - "info": ImportState.ERROR, - } - - def test_with_non_matching_verbose_users_with_errors(self) -> None: - self.setup_meeting_with_settings(123) - self.create_user("anotherOne", [123]) - knights = [ - "Sir Lancelot the Brave", - "Sir Galahad the Pure", - "Sir Bedivere the Wise", - "Sir Robin the-not-quite-so-brave-as-Sir-Lancelot", - "Arthur, King of the Britons", - ] - response = self.request( - "motion.json_upload", - { - "data": [ - { - "title": "New", - "text": "motion", - "submitters_username": ["user123", "anotherOne"], - "submitters_verbose": knights, - "supporters_username": ["user123", "anotherOne"], - "supporters_verbose": knights, - }, - ], - "meeting_id": 123, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 1 - result = response.json["results"][0][0] - assert result["state"] == ImportState.ERROR - row = result["rows"][0] - assert row["state"] == ImportState.ERROR - assert row["messages"] == [ - "Error: Verbose field is set and has more entries than the username field for submitters", - "Error: Verbose field is set and has more entries than the username field for supporters", - ] - assert row["data"]["submitters_username"] == [ - {"info": ImportState.ERROR, "value": "user123"}, - {"info": ImportState.ERROR, "value": "anotherOne"}, - ] - assert row["data"]["supporters_username"] == [ - {"info": ImportState.ERROR, "value": "user123"}, - {"info": ImportState.ERROR, "value": "anotherOne"}, - ] - - -class MotionJsonUploadForUseInImport(BaseMotionJsonUpload): - def json_upload_amendment(self) -> None: - self.create_meeting(42) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "text": "my", "motion_amendment": "1"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 1 - assert response.json["results"][0][0]["state"] == ImportState.WARNING - data = { - "meeting_id": 42, - "title": {"value": "test", "info": ImportState.DONE}, - "text": {"value": "

my

", "info": ImportState.DONE}, - "motion_amendment": {"value": True, "info": ImportState.WARNING}, - "submitters_username": [{"id": 1, "info": "generated", "value": "admin"}], - } - expected = { - "state": ImportState.NEW, - "messages": ["Amendments cannot be correctly imported"], - "data": data, - } - assert response.json["results"][0][0]["rows"][0] == expected - - def json_upload_create_missing_title(self) -> None: - self.setup_meeting_with_settings(42) - response = self.request( - "motion.json_upload", - { - "data": [{"text": "my", "reason": "stuff"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.ERROR - assert ( - "Error: Title is required" - in response.json["results"][0][0]["rows"][0]["messages"] - ) - - assert response.json["results"][0][0]["rows"][0]["data"]["title"] == { - "value": "", - "info": ImportState.ERROR, - } - - def json_upload_update_missing_title(self) -> None: - self.setup_meeting_with_settings(42) - response = self.request( - "motion.json_upload", - { - "data": [{"text": "my", "reason": "stuff", "number": "NUM01"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.DONE - assert response.json["results"][0][0]["rows"][0]["messages"] == [] - assert "title" not in response.json["results"][0][0]["rows"][0]["data"] - - def json_upload_update_missing_reason_although_required(self) -> None: - self.setup_meeting_with_settings(42, is_reason_required=True) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "text": "my", "number": "NUM01"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.DONE - assert response.json["results"][0][0]["rows"][0]["messages"] == [] - assert "reason" not in response.json["results"][0][0]["rows"][0]["data"] - - def json_upload_multi_row(self) -> None: - self.setup_meeting_with_settings(1, is_set_number=True) - self.set_user_groups(1, [1]) - self.create_user("anotherUser", [1]) - self.set_models( - { - "meeting/1": {"tag_ids": [1], "motion_block_ids": [1]}, - "tag/1": {"meeting_id": 1, "name": "Tag1"}, - "motion_block/1": {"meeting_id": 1, "title": "Block1"}, - } - ) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "first", - "text": "test my stuff", - "supporters_username": ["user1"], - }, - { - "number": "NUM02", - "title": "then", - "reason": "test my other stuff", - "category_name": "Other motion", - "submitters_username": ["user1"], - "submitters_verbose": ["Lancelot the brave"], - }, - { - "number": "NUM03", - "title": "also", - "text": "test the other peoples stuff", - "tags": ["Tag1"], - }, - { - "title": "after that", - "text": "test even more stuff", - "supporters_username": ["user1", "admin", "anotherUser"], - "block": "Block1", - }, - { - "title": "finally", - "text": "finish testing", - "category_name": "Other motion", - "category_prefix": "OTHER", - }, - ], - "meeting_id": 1, - }, - ) - self.assert_status_code(response, 200) - rows = response.json["results"][0][0]["rows"] - simple_payload_addition = { - "meeting_id": 1, - "submitters_username": [ - {"id": 1, "info": ImportState.GENERATED, "value": "admin"} - ], - } - assert len(rows) == 5 - row = rows[0] - assert row["state"] == ImportState.DONE - assert row["messages"] == [] - assert row["data"] == { - **simple_payload_addition, - "id": 1, - "number": {"value": "NUM01", "info": ImportState.DONE, "id": 1}, - "title": {"info": ImportState.DONE, "value": "first"}, - "text": {"info": ImportState.DONE, "value": "

test my stuff

"}, - "supporters_username": [ - {"id": 2, "info": ImportState.DONE, "value": "user1"} - ], - } - row = rows[1] - assert row["state"] == ImportState.DONE - assert row["messages"] == [] - assert row["data"] == { - **simple_payload_addition, - "id": 100, - "number": {"value": "NUM02", "info": ImportState.DONE, "id": 100}, - "title": {"info": ImportState.DONE, "value": "then"}, - "reason": {"info": ImportState.DONE, "value": "test my other stuff"}, - "category_name": { - "info": ImportState.DONE, - "value": "Other motion", - "id": 1000, - }, - "submitters_username": [ - {"id": 2, "info": ImportState.DONE, "value": "user1"} - ], - "submitters_verbose": ["Lancelot the brave"], - } - row = rows[2] - assert row["state"] == ImportState.NEW - assert row["messages"] == [] - assert row["data"] == { - **simple_payload_addition, - "number": {"value": "NUM03", "info": ImportState.DONE}, - "title": {"info": ImportState.DONE, "value": "also"}, - "text": { - "info": ImportState.DONE, - "value": "

test the other peoples stuff

", - }, - "tags": [{"id": 1, "info": ImportState.DONE, "value": "Tag1"}], - } - row = rows[3] - assert row["state"] == ImportState.NEW - assert row["messages"] == [] - assert row["data"] == { - **simple_payload_addition, - "number": {"value": "03", "info": ImportState.GENERATED}, - "title": {"info": ImportState.DONE, "value": "after that"}, - "text": {"info": ImportState.DONE, "value": "

test even more stuff

"}, - "supporters_username": [ - {"id": 2, "info": ImportState.DONE, "value": "user1"}, - {"id": 1, "info": ImportState.DONE, "value": "admin"}, - {"id": 3, "info": ImportState.DONE, "value": "anotherUser"}, - ], - "block": {"id": 1, "info": ImportState.DONE, "value": "Block1"}, - } - row = rows[4] - assert row["state"] == ImportState.NEW - assert row["messages"] == [] - assert row["data"] == { - **simple_payload_addition, - "number": {"value": "OTHER01", "info": ImportState.GENERATED}, - "title": {"info": ImportState.DONE, "value": "finally"}, - "text": {"info": ImportState.DONE, "value": "

finish testing

"}, - "category_name": { - "info": ImportState.DONE, - "value": "Other motion", - "id": 100, - }, - "category_prefix": "OTHER", - } - - def json_upload_simple_create( - self, - is_reason_required: bool = False, - is_set_number: bool = False, - ) -> None: - self.setup_meeting_with_settings(42, is_reason_required, is_set_number) - response = self.request( - "motion.json_upload", - { - "data": [{"title": "test", "text": "my", "reason": "stuff"}], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 1 - data = { - "meeting_id": 42, - "title": {"value": "test", "info": ImportState.DONE}, - "text": {"value": "

my

", "info": ImportState.DONE}, - "reason": {"value": "stuff", "info": ImportState.DONE}, - "submitters_username": [{"id": 1, "info": "generated", "value": "admin"}], - } - if is_set_number: - data.update({"number": {"info": ImportState.GENERATED, "value": "03"}}) - expected = { - "state": ImportState.NEW, - "messages": [], - "data": data, - } - assert response.json["results"][0][0]["rows"][0] == expected - - def json_upload_simple_update( - self, - is_reason_required: bool = False, - is_set_number: bool = False, - ) -> None: - self.setup_meeting_with_settings(42, is_reason_required, is_set_number) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "test", - "text": "my", - "reason": "stuff", - } - ], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 1 - expected = { - "state": ImportState.DONE, - "messages": [], - "data": { - "id": 42, - "meeting_id": 42, - "number": {"id": 42, "value": "NUM01", "info": ImportState.DONE}, - "title": {"value": "test", "info": ImportState.DONE}, - "text": {"value": "

my

", "info": ImportState.DONE}, - "reason": {"value": "stuff", "info": ImportState.DONE}, - "submitters_username": [ - {"id": 1, "info": "generated", "value": "admin"} - ], - }, - } - assert response.json["results"][0][0]["rows"][0] == expected - - def json_upload_update_with_foreign_meeting(self) -> None: - self.setup_meeting_with_settings(42, is_set_number=True) - self.setup_meeting_with_settings(55) - self.create_user("orgaUser") - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "test", - "text": "my", - "reason": "stuff", - "category_name": "Normal motion", - "category_prefix": "NORM", - "submitters_username": ["user55"], - "supporters_username": ["user42", "nonExistant", "orgaUser"], - } - ], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - assert response.json["results"][0][0]["state"] == ImportState.WARNING - assert response.json["results"][0][0]["headers"] == [ - {"property": "title", "type": "string", "is_object": True}, - {"property": "text", "type": "string", "is_object": True}, - {"property": "number", "type": "string", "is_object": True}, - {"property": "reason", "type": "string", "is_object": True}, - { - "property": "submitters_verbose", - "type": "string", - "is_list": True, - "is_hidden": True, - }, - { - "property": "submitters_username", - "type": "string", - "is_object": True, - "is_list": True, - }, - { - "property": "supporters_verbose", - "type": "string", - "is_list": True, - "is_hidden": True, - }, - { - "property": "supporters_username", - "type": "string", - "is_object": True, - "is_list": True, - }, - {"property": "category_name", "type": "string", "is_object": True}, - {"property": "category_prefix", "type": "string"}, - {"property": "tags", "type": "string", "is_object": True, "is_list": True}, - {"property": "block", "type": "string", "is_object": True}, - { - "property": "motion_amendment", - "type": "boolean", - "is_object": True, - "is_hidden": True, - }, - ] - assert response.json["results"][0][0]["statistics"] == [ - {"name": "total", "value": 1}, - {"name": "created", "value": 0}, - {"name": "updated", "value": 1}, - {"name": "error", "value": 0}, - {"name": "warning", "value": 1}, - ] - assert len(response.json["results"][0][0]["rows"]) == 1 - assert response.json["results"][0][0]["rows"][0]["state"] == ImportState.DONE - messages = response.json["results"][0][0]["rows"][0]["messages"] - assert len(messages) == 2 - assert messages[0] == "Could not find at least one submitter: user55" - assert messages[1].startswith("Could not find at least one supporter:") - assert " nonExistant" in messages[1] - assert " orgaUser" in messages[1] - assert response.json["results"][0][0]["rows"][0]["data"] == { - "number": {"value": "NUM01", "info": ImportState.DONE, "id": 42}, - "title": {"value": "test", "info": ImportState.DONE}, - "text": {"value": "

my

", "info": ImportState.DONE}, - "reason": {"value": "stuff", "info": ImportState.DONE}, - "category_name": { - "value": "Normal motion", - "info": ImportState.DONE, - "id": 42, - }, - "category_prefix": "NORM", - "meeting_id": 42, - "submitters_username": [ - {"value": "user55", "info": ImportState.WARNING}, - {"id": 1, "info": ImportState.GENERATED, "value": "admin"}, - ], - "id": 42, - "supporters_username": [ - {"value": "user42", "info": ImportState.DONE, "id": 2}, - {"value": "nonExistant", "info": ImportState.WARNING}, - {"value": "orgaUser", "info": ImportState.WARNING}, - ], - } - - def json_upload_custom_number_create(self) -> None: - self.assert_custom_number_create() - - def json_upload_custom_number_create_with_set_number(self) -> None: - self.assert_custom_number_create(True) - - def assert_custom_number_create(self, is_set_number: bool = False) -> None: - self.setup_meeting_with_settings(42, is_set_number) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "Z01", - "title": "test", - "text": "my", - "reason": "stuff", - "category_name": "Other motion", - "category_prefix": "NORM", - } - ], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - rows = response.json["results"][0][0]["rows"] - assert len(rows) == 1 - assert rows[0] == { - "state": ImportState.NEW, - "messages": [], - "data": { - "meeting_id": 42, - "number": {"value": "Z01", "info": ImportState.DONE}, - "title": {"value": "test", "info": ImportState.DONE}, - "text": {"value": "

my

", "info": ImportState.DONE}, - "reason": {"value": "stuff", "info": ImportState.DONE}, - "submitters_username": [ - {"id": 1, "info": "generated", "value": "admin"} - ], - "category_prefix": "NORM", - "category_name": { - "info": ImportState.DONE, - "value": "Other motion", - "id": 420, - }, - }, - } - - def json_upload_with_warnings(self) -> None: - self.setup_meeting_with_settings(10) - self.set_models( - { - "motion_category/100000": { - "name": "Normal motion", - "prefix": "NORM", - "meeting_id": 10, - }, - "meeting/10": {"motion_category_ids": [10, 100, 1000, 10000, 100000]}, - } - ) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "Up", - "text": "date", - "category_name": "Unknown", - "category_prefix": "CAT", - "submitters_username": ["user10", "user10"], - }, - { - "title": "New", - "text": "motion", - "category_prefix": "Shouldn't be found", - "supporters_username": ["user10", "user10", "user10", "user10"], - }, - { - "title": "Newer", - "text": "motion", - "category_name": "Normal motion", - "category_prefix": "NORM", - "submitters_username": ["nonExistant"], - "supporters_username": ["nonExistant", "nonExistant"], - }, - ], - "meeting_id": 10, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 3 - result = response.json["results"][0][0] - assert result["state"] == ImportState.WARNING - row = result["rows"][0] - assert row["state"] == ImportState.DONE - assert sorted(row["messages"]) == sorted( - [ - "Category could not be found", - "At least one submitter has been referenced multiple times: user10", - ] - ) - assert row["data"] == { - "number": {"value": "NUM01", "info": ImportState.DONE, "id": 10}, - "title": {"value": "Up", "info": ImportState.DONE}, - "text": {"value": "

date

", "info": ImportState.DONE}, - "category_name": {"value": "Unknown", "info": ImportState.WARNING}, - "category_prefix": "CAT", - "meeting_id": 10, - "submitters_username": [ - {"value": "user10", "info": ImportState.DONE, "id": 2}, - {"value": "user10", "info": ImportState.WARNING}, - ], - "id": 10, - } - row = result["rows"][1] - assert row["state"] == ImportState.NEW - assert sorted(row["messages"]) == sorted( - [ - "Category could not be found", - "At least one supporter has been referenced multiple times: user10", - ] - ) - assert row["data"] == { - "title": {"value": "New", "info": ImportState.DONE}, - "text": {"value": "

motion

", "info": ImportState.DONE}, - "category_name": {"value": "", "info": ImportState.WARNING}, - "category_prefix": "Shouldn't be found", - "meeting_id": 10, - "submitters_username": [ - {"value": "admin", "info": ImportState.GENERATED, "id": 1} - ], - "supporters_username": [ - {"value": "user10", "info": ImportState.DONE, "id": 2}, - {"value": "user10", "info": ImportState.WARNING}, - {"value": "user10", "info": ImportState.WARNING}, - {"value": "user10", "info": ImportState.WARNING}, - ], - } - row = result["rows"][2] - assert row["state"] == ImportState.NEW - assert sorted(row["messages"]) == sorted( - [ - "Category could not be found", - "Could not find at least one submitter: nonExistant", - "At least one supporter has been referenced multiple times: nonExistant", - "Could not find at least one supporter: nonExistant", - ] - ) - assert row["data"] == { - "title": {"value": "Newer", "info": ImportState.DONE}, - "text": {"value": "

motion

", "info": ImportState.DONE}, - "category_name": {"value": "Normal motion", "info": ImportState.WARNING}, - "category_prefix": "NORM", - "meeting_id": 10, - "submitters_username": [ - {"value": "nonExistant", "info": ImportState.WARNING}, - {"value": "admin", "info": ImportState.GENERATED, "id": 1}, - ], - "supporters_username": [ - {"value": "nonExistant", "info": ImportState.WARNING}, - {"value": "nonExistant", "info": ImportState.WARNING}, - ], - } - - def json_upload_with_non_matching_verbose_users_okay(self) -> None: - self.setup_meeting_with_settings(123) - self.create_user("anotherOne", [123]) - knights = [ - "Sir Lancelot the Brave", - "Sir Galahad the Pure", - "Sir Bedivere the Wise", - "Sir Robin the-not-quite-so-brave-as-Sir-Lancelot", - "Arthur, King of the Britons", - ] - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "Up", - "text": "date", - "submitters_username": ["user123", "anotherOne"], - "submitters_verbose": [knights[0]], - "supporters_username": ["user123", "anotherOne"], - "supporters_verbose": [knights[0]], - }, - { - "title": "Newer", - "text": "motion", - "submitters_verbose": knights, - "supporters_verbose": knights, - }, - ], - "meeting_id": 123, - }, - ) - self.assert_status_code(response, 200) - assert len(response.json["results"][0][0]["rows"]) == 2 - result = response.json["results"][0][0] - assert result["state"] == ImportState.DONE - row = result["rows"][0] - assert row["state"] == ImportState.DONE - assert row["messages"] == [] - assert row["data"]["submitters_username"] == [ - {"id": 2, "info": ImportState.DONE, "value": "user123"}, - {"id": 3, "info": ImportState.DONE, "value": "anotherOne"}, - ] - assert row["data"]["supporters_username"] == [ - {"id": 2, "info": ImportState.DONE, "value": "user123"}, - {"id": 3, "info": ImportState.DONE, "value": "anotherOne"}, - ] - row = result["rows"][1] - assert row["state"] == ImportState.NEW - assert row["messages"] == [] - assert row["data"]["submitters_username"] == [ - {"value": "admin", "info": ImportState.GENERATED, "id": 1} - ] - assert row["data"]["submitters_verbose"] == knights - assert "supporters_username" not in row["data"] - assert row["data"]["supporters_verbose"] == knights - - def json_upload_with_tags_and_blocks(self) -> None: - self.setup_meeting_with_settings(42) - self.setup_meeting_with_settings(55) - self.set_models( - { - "meeting/42": { - "tag_ids": [1, 2, 3, 4], - "motion_block_ids": [1, 2, 3, 4], - }, - "meeting/55": {"tag_ids": [5, 6], "motion_block_ids": [5, 6]}, - "tag/1": {"name": "Tag-liatelle", "meeting_id": 42}, - "tag/2": {"name": "Tag-you're-it", "meeting_id": 42}, - "tag/3": {"name": "Tag-ether", "meeting_id": 42}, - "tag/4": {"name": "Tag-ether", "meeting_id": 42}, - "tag/5": {"name": "Tag-you're-it", "meeting_id": 55}, - "tag/6": {"name": "Price tag", "meeting_id": 55}, - "motion_block/1": {"title": "Blockolade", "meeting_id": 42}, - "motion_block/2": {"title": "Blockodile", "meeting_id": 42}, - "motion_block/3": {"title": "Block chain", "meeting_id": 42}, - "motion_block/4": {"title": "Block chain", "meeting_id": 42}, - "motion_block/5": {"title": "Blockodile", "meeting_id": 55}, - "motion_block/6": {"title": "Blockoli", "meeting_id": 55}, - } - ) - response = self.request( - "motion.json_upload", - { - "data": [ - { - "number": "NUM01", - "title": "Up", - "text": "date", - "tags": [ - "Tag-liatelle", - "Tag-you're-it", - "Tag-ether", - "Price tag", - "Not a tag", - ], - "block": "Blockolade", - }, - { - "title": "New", - "text": "motion", - "tags": [ - "Tag-liatelle", - "Tag-liatelle", - "Tag-you're-it", - "Tag-you're-it", - ], - "block": "Blockodile", - }, - {"title": "Newer", "text": "motion", "block": "Block chain"}, - {"title": "Newest", "text": "motion", "block": "Blockoli"}, - ], - "meeting_id": 42, - }, - ) - self.assert_status_code(response, 200) - result = response.json["results"][0][0] - assert result["state"] == ImportState.WARNING - assert len(result["rows"]) == 4 - row = result["rows"][0] - assert row["state"] == ImportState.DONE - assert row["messages"][0].startswith("Could not find at least one tag:") - assert " Not a tag" in row["messages"][0] - assert " Price tag" in row["messages"][0] - assert row["messages"][1] == "Found multiple tags with the same name: Tag-ether" - assert row["data"]["tags"] == [ - {"value": "Tag-liatelle", "info": "done", "id": 1}, - {"value": "Tag-you're-it", "info": "done", "id": 2}, - {"value": "Tag-ether", "info": "warning"}, - {"value": "Price tag", "info": "warning"}, - {"value": "Not a tag", "info": "warning"}, - ] - assert row["data"]["block"] == {"value": "Blockolade", "info": "done", "id": 1} - row = result["rows"][1] - assert row["state"] == ImportState.NEW - assert len(row["messages"]) == 1 - assert row["messages"][0].startswith( - "At least one tag has been referenced multiple times:" - ) - assert "Tag-liatelle" in row["messages"][0] - assert "Tag-you're-it" in row["messages"][0] - assert row["data"]["tags"] == [ - {"value": "Tag-liatelle", "info": "done", "id": 1}, - {"value": "Tag-liatelle", "info": ImportState.WARNING}, - {"value": "Tag-you're-it", "info": "done", "id": 2}, - {"value": "Tag-you're-it", "info": ImportState.WARNING}, - ] - assert row["data"]["block"] == {"value": "Blockodile", "info": "done", "id": 2} - row = result["rows"][2] - assert row["state"] == ImportState.NEW - assert row["messages"] == ["Found multiple motion blocks with the same name"] - assert row["data"]["block"] == { - "value": "Block chain", - "info": ImportState.WARNING, - } - row = result["rows"][3] - assert row["state"] == ImportState.NEW - assert row["messages"] == ["Could not find motion block"] - assert row["data"]["block"] == { - "value": "Blockoli", - "info": ImportState.WARNING, - }