From cabba247f318a2cb9acfc8b4ba40767ccd8b55d6 Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Fri, 5 Feb 2021 17:08:29 +0100 Subject: [PATCH] Reworked checker Credits for initial work to @reiterl --- .github/workflows/models.yml | 26 +- docker/initial-data.json | 4 +- docs/datavalidator/check_json.py | 384 ++++++++++++++++++++++++++++ docs/datavalidator/requirements.txt | 2 + docs/datavalidator/setup.cfg | 6 + docs/example-data.json | 122 ++++++--- docs/modelsvalidator/check_json.py | 100 -------- 7 files changed, 500 insertions(+), 144 deletions(-) create mode 100644 docs/datavalidator/check_json.py create mode 100644 docs/datavalidator/requirements.txt create mode 100644 docs/datavalidator/setup.cfg delete mode 100644 docs/modelsvalidator/check_json.py diff --git a/.github/workflows/models.yml b/.github/workflows/models.yml index 1fbbfa09a..874cb7c43 100644 --- a/.github/workflows/models.yml +++ b/.github/workflows/models.yml @@ -1,7 +1,9 @@ name: Validate models.yml on: [push, pull_request] +env: + PYTHON_VERSION: 3.8.5 jobs: - validate: + validate-models: name: Validate models.yml runs-on: ubuntu-latest steps: @@ -20,4 +22,24 @@ jobs: GO111MODULE: on - name: Validate models.yml - run: docs/modelsvalidator/modelsvalidator docs/models.yml \ No newline at end of file + run: docs/modelsvalidator/modelsvalidator docs/models.yml + + validate-data: + name: Validate example-data.json and initial-data.json + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install requirements + working-directory: docs/datavalidator + run: pip install -U -r requirements.txt + + - name: Validate + working-directory: docs/datavalidator + run: python check_json.py diff --git a/docker/initial-data.json b/docker/initial-data.json index 4bb2cca56..4c70f899b 100644 --- a/docker/initial-data.json +++ b/docker/initial-data.json @@ -248,7 +248,7 @@ "temporary_user_ids": [], "guest_ids": [], "user_ids": [1], - "reference_projector_id": 2, + "reference_projector_id": 1, "default_group_id": 1, "admin_group_id": 2 @@ -546,7 +546,7 @@ "current_element_ids": [], "preview_projection_ids": [], "history_projection_ids": [], - "used_as_reference_projector_meeting_id": null, + "used_as_reference_projector_meeting_id": 1, "projectiondefault_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], "meeting_id": 1 }], diff --git a/docs/datavalidator/check_json.py b/docs/datavalidator/check_json.py new file mode 100644 index 000000000..9fcd794a4 --- /dev/null +++ b/docs/datavalidator/check_json.py @@ -0,0 +1,384 @@ +import json +import re +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple + +import fastjsonschema +import yaml + +MODELS_YML_PATH = "../../docs/models.yml" + +CHECKED_FILES = [ + "../../docker/initial-data.json", + "../../docs/example-data.json", +] + +SCHEMA = fastjsonschema.compile( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Schema for initial and example data.", + "type": "object", + "patternProperties": { + "^[a-z_]+$": { + "type": "array", + "items": { + "type": "object", + "properties": {"id": {"type": "number"}}, + "required": ["id"], + }, + } + }, + "additionalProperties": False, + } +) + + +class CheckException(Exception): + pass + + +def check_string(value: Any) -> bool: + return value is None or isinstance(value, str) + + +def check_number(value: Any) -> bool: + return value is None or isinstance(value, int) + + +def check_boolean(value: Any) -> bool: + return value is None or value is False or value is True + + +def check_string_list(value: Any) -> bool: + return check_x_list(value, check_string) + + +def check_number_list(value: Any) -> bool: + return check_x_list(value, check_number) + + +def check_x_list(value: Any, fn: Callable) -> bool: + if value is None: + return True + if not isinstance(value, list): + return False + return all([fn(sv) for sv in value]) + + +def check_decimal(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str): + pattern = r"^-?(\d|[1-9]\d+)\.\d{6}$" + if re.match(pattern, value): + return True + return False + + +class Checker: + def __init__(self, data: Dict[str, List[Any]]) -> None: + self.data = data + + with open(MODELS_YML_PATH, "rb") as x: + models_yml = x.read() + models_yml = models_yml.replace(" yes:".encode(), ' "yes":'.encode()) + models_yml = models_yml.replace(" no:".encode(), ' "no":'.encode()) + self.models = yaml.safe_load(models_yml) + + self.errors: List[str] = [] + + def run_check(self) -> None: + self.check_json() + self.check_collections() + for collection, models in self.data.items(): + for model in models: + self.check_model(collection, model) + if self.errors: + errors = [f"\t{error}" for error in self.errors] + raise CheckException("\n".join(errors)) + + def check_json(self) -> None: + try: + SCHEMA(self.data) + except fastjsonschema.exceptions.JsonSchemaException as e: + raise CheckException(f"JSON does not match schema: {str(e)}") + + def check_collections(self) -> None: + c1 = set(self.data.keys()) + c2 = set(self.models.keys()) + if c1 != c2: + err = "Collections in JSON file do not match with models.yml." + if c2 - c1: + err += f" Missing collections: {', '.join(c2-c1)}." + if c1 - c2: + err += f" Invalid collections: {', '.join(c1-c2)}." + raise CheckException(err) + + def check_model(self, collection: str, model: Dict[str, Any]) -> None: + model_fields = set(x for x in model.keys() if "$" not in x) + collection_fields = set( + x for x in self.models[collection].keys() if "$" not in x + ) + + errors = False + if collection_fields - model_fields: + error = f"{collection}/{model['id']}: Missing fields {', '.join(collection_fields - model_fields)}" + self.errors.append(error) + errors = True + if model_fields - collection_fields: + error = f"{collection}/{model['id']}: Invalid fields {', '.join(model_fields - collection_fields)}" + self.errors.append(error) + errors = True + + if not errors: + self.check_types(model, collection) + self.check_relations(model, collection) + + def check_types(self, model: Dict[str, Any], collection: str) -> None: + for field in model.keys(): + if "$" in field: + continue + + checker = None + field_type = self.get_type_from_collection(field, collection) + if field_type in ( + "string", + "HTMLStrict", + "HTMLPermissive", + "generic-relation", + ): + checker = check_string + elif field_type in ("number", "timestamp", "relation"): + checker = check_number + elif field_type == "boolean": + checker = check_boolean + elif field_type in ("string[]", "generic-relation-list"): + checker = check_string_list + elif field_type in ("number[]", "relation-list"): + checker = check_number_list + elif field_type == "decimal(6)": + checker = check_decimal + elif field_type in ( + "JSON", + "template", + ): + pass + else: + raise NotImplementedError(f"TODO field type {field_type}") + if checker is not None and not checker(model[field]): + error = f"{collection}/{model['id']}/{field}: Type error: Type is not {field_type}" + self.errors.append(error) + + def get_type_from_collection(self, field: str, collection: str) -> str: + field_value = self.models[collection][field] + if isinstance(field_value, dict): + return field_value["type"] + return field_value + + def check_relations(self, model: Dict[str, Any], collection: str) -> None: + for field in model.keys(): + try: + self.check_relation(model, collection, field) + except CheckException as e: + self.errors.append( + f"{collection}/{model['id']}/{field} error: " + str(e) + ) + + def check_relation( + self, model: Dict[str, Any], collection: str, field: str + ) -> None: + if "$" in field: + return + + field_type = self.get_type_from_collection(field, collection) + basemsg = self.get_basemsg(collection, model["id"], field) + + if field_type == "relation": + foreign_id = model[field] + if foreign_id is None: + return + + foreign_collection, foreign_field = self.get_to(field, collection) + if "$" in foreign_field: + return + + self.check_reverse_relation( + collection, + model["id"], + foreign_collection, + foreign_id, + foreign_field, + basemsg, + ) + + elif field_type == "relation-list": + foreign_ids = model[field] + if foreign_ids is None: + return + + foreign_collection, foreign_field = self.get_to(field, collection) + if "$" in foreign_field: + return + + for foreign_id in foreign_ids: + self.check_reverse_relation( + collection, + model["id"], + foreign_collection, + foreign_id, + foreign_field, + basemsg, + ) + + elif field_type == "generic-relation" and model[field] is not None: + foreign_collection, foreign_id = self.split_fqid(model[field]) + foreign_field = self.get_to_generic_case( + collection, field, foreign_collection + ) + + if "$" in foreign_field: + return + + self.check_reverse_relation( + collection, + model["id"], + foreign_collection, + foreign_id, + foreign_field, + basemsg, + ) + + elif field_type == "generic-relation-list" and model[field] is not None: + for fqid in model[field]: + foreign_collection, foreign_id = self.split_fqid(fqid) + foreign_field = self.get_to_generic_case( + collection, field, foreign_collection + ) + + if "$" in foreign_field: + continue + + self.check_reverse_relation( + collection, + model["id"], + foreign_collection, + foreign_id, + foreign_field, + basemsg, + ) + + def get_to(self, field: str, collection: str) -> Tuple[str, str]: + to = self.models[collection][field]["to"] + return to.split("/") + + def get_value(self, collection: str, id: int, field: str) -> Any: + model = self.find_model(collection, id) + if model is None: + return None + return model.get(field) + + def find_model(self, collection: str, id: int) -> Optional[Dict[str, Any]]: + c = self.data.get(collection, []) + for model in c: + if model["id"] == id: + return model + return None + + def get_basemsg(self, collection: str, id: int, field: str) -> str: + return f"{collection}/{id}/{field}: RelationError: " + + def check_reverse_relation( + self, + collection: str, + id: int, + foreign_collection: str, + foreign_id: int, + foreign_field: str, + basemsg: str, + ) -> None: + foreign_value = self.get_value(foreign_collection, foreign_id, foreign_field) + foreign_field_type = self.get_type_from_collection( + foreign_field, foreign_collection + ) + fqid = f"{collection}/{id}" + error = False + if foreign_field_type == "relation": + error = foreign_value != id + elif foreign_field_type == "relation-list": + error = not foreign_value or id not in foreign_value + elif foreign_field_type == "generic-relation": + error = foreign_value != fqid + elif foreign_field_type == "generic-relation-list": + error = not foreign_value or fqid not in foreign_value + else: + raise NotImplementedError() + + if error: + self.errors.append( + f"{basemsg} points to {foreign_collection}/{foreign_id}/{foreign_field}," + " but the reverse relation for is corrupt" + ) + + def split_fqid(self, fqid: str) -> Tuple[str, int]: + try: + collection, _id = fqid.split("/") + id = int(_id) + if collection not in self.models.keys(): + raise CheckException(f"Fqid {fqid} has an invalid collection") + return collection, id + except ValueError: + raise CheckException(f"Fqid {fqid} is malformed") + + def split_collectionfield(self, collectionfield: str) -> Tuple[str, str]: + collection, field = collectionfield.split("/") + if collection not in self.models.keys(): + raise CheckException( + f"Collectionfield {collectionfield} has an invalid collection" + ) + if ( + field not in self.models[collection] + ): # Note: this has to be adopted when supporting template fields + raise CheckException( + f"Collectionfield {collectionfield} has an invalid field" + ) + return collection, field + + def get_to_generic_case( + self, collection: str, field: str, foreign_collection: str + ) -> str: + """ Returns all reverse relations as collectionfields """ + to = self.models[collection][field]["to"] + if isinstance(to, dict): + if foreign_collection not in to["collections"]: + raise CheckException( + f"The collection {foreign_collection} is not supported " + "as a reverse relation in {collection}/{field}" + ) + return to["field"] + + for cf in to: + c, f = self.split_collectionfield(cf) + if c == foreign_collection: + return f + + raise CheckException( + f"The collection {foreign_collection} is not supported as a reverse relation in {collection}/{field}" + ) + + +def main() -> int: + failed = False + for f in CHECKED_FILES: + with open(f) as data: + try: + Checker(json.load(data)).run_check() + except CheckException as e: + print(f"Check for {f} failed:\n", e) + failed = True + else: + print(f"Check for {f} successful.") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/datavalidator/requirements.txt b/docs/datavalidator/requirements.txt new file mode 100644 index 000000000..da27c7d17 --- /dev/null +++ b/docs/datavalidator/requirements.txt @@ -0,0 +1,2 @@ +fastjsonschema +pyyaml diff --git a/docs/datavalidator/setup.cfg b/docs/datavalidator/setup.cfg new file mode 100644 index 000000000..261299cc3 --- /dev/null +++ b/docs/datavalidator/setup.cfg @@ -0,0 +1,6 @@ +[flake8] +max-line-length = 120 + +[mypy] +disallow_untyped_defs = true +ignore_missing_imports = true diff --git a/docs/example-data.json b/docs/example-data.json index 5d4725646..7858b6619 100644 --- a/docs/example-data.json +++ b/docs/example-data.json @@ -7,6 +7,10 @@ "login_text": "Guten Morgen!", "theme": "openslides-theme", "custom_translations": [], + "enable_electronic_voting": true, + "reset_password_verbose_errors": true, + "name": "Test Organization", + "description": "", "committee_ids": [1], "resource_ids": [1] @@ -28,6 +32,7 @@ "default_structure_level": "", "default_vote_weight": "1.000000", "last_email_send": null, + "is_demo_user": false, "organisation_management_level": "superadmin", @@ -88,6 +93,7 @@ "default_structure_level": "", "default_vote_weight": "1.000000", "last_email_send": null, + "is_demo_user": false, "organisation_management_level": "", @@ -143,6 +149,7 @@ "default_structure_level": "", "default_vote_weight": "1.000000", "last_email_send": null, + "is_demo_user": false, "organisation_management_level": "", @@ -230,13 +237,17 @@ "conference_open_video": true, "conference_auto_connect_next_speakers": true, + "jitsi_room_name": "", + "jitsi_domain": "", + "jitsi_room_password": "", + "projector_default_countdown_time": 60, "projector_countdown_warning_time": 0, "export_csv_encoding": "utf-8", "export_csv_separator": ",", "export_pdf_pagenumber_alignment": "center", - "export_pdf_fontsize": "10", + "export_pdf_fontsize": 10, "export_pdf_pagesize": "A4", "agenda_show_subtitles": false, @@ -333,7 +344,7 @@ "poll_default_group_ids": [3], "projector_ids": [1, 2], - "projection_ids": [1, 2, 4, 6], + "projection_ids": [1, 2, 4], "projectiondefault_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "projector_message_ids": [1], "projector_countdown_ids": [1], @@ -342,15 +353,18 @@ "list_of_speakers_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], "speaker_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], "topic_ids": [1, 2, 3, 4, 5, 6, 7, 8], - "group_ids": [1, 2, 3, 5, 6], + "group_ids": [1, 2, 3, 4, 5], "mediafile_ids": [1, 2, 3], "motion_ids": [1, 2, 3, 4], "motion_submitter_ids": [1, 2, 3, 4], "motion_comment_section_ids": [1], + "motion_comment_ids": [1], + "motion_state_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "motion_category_ids": [1, 2], "motion_block_ids": [1], "motion_workflow_ids": [1, 2], "motion_statute_paragraph_ids": [], + "motion_change_recommendation_ids": [4, 5], "poll_ids": [1, 2, 3, 4, 5], "option_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], "vote_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9], @@ -392,6 +406,7 @@ "user_ids": [], "mediafile_access_group_ids": [], + "mediafile_inherited_access_group_ids": [], "read_comment_section_ids": [], "write_comment_section_ids": [], "poll_ids": [], @@ -458,7 +473,7 @@ "meeting_id": 1 }, { - "id": 5, + "id": 4, "name": "Committees", "admin_group_for_meeting_id": null, "default_group_for_meeting_id": null, @@ -477,16 +492,17 @@ "user_ids": [], "mediafile_access_group_ids": [], + "mediafile_inherited_access_group_ids": [], "read_comment_section_ids": [], "write_comment_section_ids": [], "poll_ids": [], "used_as_motion_poll_default_id": null, - "used_as_assignment_poll_default_id": 1, + "used_as_assignment_poll_default_id": null, "used_as_poll_default_id": null, "meeting_id": 1 }, { - "id": 6, + "id": 5, "name": "Delegates", "admin_group_for_meeting_id": null, "default_group_for_meeting_id": null, @@ -509,11 +525,12 @@ "user_ids": [2], "mediafile_access_group_ids": [], + "mediafile_inherited_access_group_ids": [], "read_comment_section_ids": [1], "write_comment_section_ids": [1], "poll_ids": [], "used_as_motion_poll_default_id": null, - "used_as_assignment_poll_default_id": null, + "used_as_assignment_poll_default_id": 1, "used_as_poll_default_id": null, "meeting_id": 1 }], @@ -1198,6 +1215,9 @@ "agenda_item_id": 3, "list_of_speakers_id": 3, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1209,6 +1229,9 @@ "agenda_item_id": 4, "list_of_speakers_id": 4, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1220,6 +1243,9 @@ "agenda_item_id": 5, "list_of_speakers_id": 5, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1231,6 +1257,9 @@ "agenda_item_id": 6, "list_of_speakers_id": 6, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1242,6 +1271,9 @@ "agenda_item_id": 7, "list_of_speakers_id": 7, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1253,6 +1285,9 @@ "agenda_item_id": 8, "list_of_speakers_id": 8, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1264,6 +1299,9 @@ "agenda_item_id": 9, "list_of_speakers_id": 9, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }, { @@ -1275,12 +1313,16 @@ "agenda_item_id": 10, "list_of_speakers_id": 10, "tag_ids": [], + "option_ids": [], + "projection_ids": [], + "current_projector_ids": [], "meeting_id": 1 }], "motion": [ { "id": 1, "number": "A1", + "number_value": 1, "sequential_number": 1, "title": "test", "text": "", @@ -1326,6 +1368,7 @@ { "id": 2, "number": "1 - 1", + "number_value": 1, "sequential_number": 2, "title": "Änderungsantrag zu 1", "text": "

lömk

", @@ -1365,12 +1408,13 @@ "attachment_ids": [], "projection_ids": [], "current_projector_ids": [], - "personal_note_ids": [], + "personal_note_ids": [1], "meeting_id": 1 }, { "id": 3, "number": "2", + "number_value": 2, "sequential_number": 3, "title": "ohne", "text": "

sf

", @@ -1416,6 +1460,7 @@ { "id": 4, "number": "3", + "number_value": 3, "sequential_number": 4, "title": "komplex", "text": "

sdf sdfpdfkw wef

\n\n

wepkf 

\n\n

weüpfk 

\n\n

weüpfdfg

", @@ -1448,7 +1493,7 @@ "option_ids": [], "change_recommendation_ids": [4], "statute_paragraph_id": null, - "comments": [], + "comment_ids": [], "agenda_item_id": 13, "list_of_speakers_id": 13, "tag_ids": [], @@ -1507,8 +1552,8 @@ "weight": 10000, "comment_ids": [1], - "read_group_ids": [3, 6], - "write_group_ids": [3, 6], + "read_group_ids": [3, 5], + "write_group_ids": [3, 5], "meeting_id": 1 }], "motion_category": [ @@ -1921,7 +1966,6 @@ "state": "finished", "min_votes_amount": 1, "max_votes_amount": 1, - "allow_multiple_votes_per_candidate": false, "global_yes": false, "global_no": false, "global_abstain": false, @@ -1930,7 +1974,6 @@ "votesvalid": "2.000000", "votesinvalid": "9.000000", "votescast": "2.000000", - "user_has_voted": false, "content_object_id": "motion/1", "option_ids": [1], @@ -1950,7 +1993,6 @@ "state": "created", "min_votes_amount": 1, "max_votes_amount": 1, - "allow_multiple_votes_per_candidate": false, "global_yes": false, "global_no": false, "global_abstain": false, @@ -1959,7 +2001,6 @@ "votesvalid": null, "votesinvalid": null, "votescast": null, - "user_has_voted": false, "content_object_id": "motion/1", "option_ids": [3], @@ -1979,7 +2020,6 @@ "state": "created", "min_votes_amount": 1, "max_votes_amount": 1, - "allow_multiple_votes_per_candidate": false, "global_yes": false, "global_no": true, "global_abstain": true, @@ -1988,7 +2028,6 @@ "votesvalid": null, "votesinvalid": null, "votescast": null, - "user_has_voted": false, "content_object_id": "assignment/1", "voted_ids": [], @@ -2008,7 +2047,6 @@ "state": "finished", "min_votes_amount": 1, "max_votes_amount": 1, - "allow_multiple_votes_per_candidate": false, "global_yes": false, "global_no": true, "global_abstain": true, @@ -2017,7 +2055,6 @@ "votesvalid": "9.000000", "votesinvalid": "2.000000", "votescast": "16.000000", - "user_has_voted": false, "content_object_id": "assignment/1", "voted_ids": [], @@ -2037,7 +2074,6 @@ "state": "finished", "min_votes_amount": 1, "max_votes_amount": 1, - "allow_multiple_votes_per_candidate": false, "global_yes": false, "global_no": true, "global_abstain": false, @@ -2046,7 +2082,6 @@ "votesvalid": "1.000000", "votesinvalid": "0.000000", "votescast": "1.000000", - "user_has_voted": true, "content_object_id": "assignment/2", "voted_ids": [1], @@ -2064,6 +2099,7 @@ "no": "4.000000", "abstain": "1.000000", "weight": 1, + "text": null, "poll_id": 1, "used_as_global_option_in_poll_id": null, @@ -2077,10 +2113,12 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": null, "used_as_global_option_in_poll_id": 1, - "vote_ids": [0], + "content_object_id": null, + "vote_ids": [], "meeting_id": 1 }, { @@ -2089,6 +2127,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": 2, "used_as_global_option_in_poll_id": null, @@ -2102,9 +2141,11 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": null, "used_as_global_option_in_poll_id": 2, + "content_object_id": null, "vote_ids": [], "meeting_id": 1 }, @@ -2114,6 +2155,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": 3, "used_as_global_option_in_poll_id": null, @@ -2127,9 +2169,11 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": null, "used_as_global_option_in_poll_id": 3, + "content_object_id": null, "vote_ids": [], "meeting_id": 1 }, @@ -2139,6 +2183,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": 4, "used_as_global_option_in_poll_id": null, @@ -2152,6 +2197,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 2, + "text": null, "poll_id": 4, "used_as_global_option_in_poll_id": null, @@ -2165,6 +2211,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 3, + "text": null, "poll_id": 4, "used_as_global_option_in_poll_id": null, @@ -2178,9 +2225,11 @@ "no": "2.000000", "abstain": "1.000000", "weight": 1, + "text": null, "poll_id": null, "used_as_global_option_in_poll_id": 4, + "content_object_id": null, "vote_ids": [7, 8], "meeting_id": 1 }, @@ -2190,6 +2239,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": 5, "used_as_global_option_in_poll_id": null, @@ -2203,6 +2253,7 @@ "no": "0.000000", "abstain": "0.000000", "weight": 2, + "text": null, "poll_id": 5, "used_as_global_option_in_poll_id": null, @@ -2216,9 +2267,11 @@ "no": "0.000000", "abstain": "0.000000", "weight": 1, + "text": null, "poll_id": null, "used_as_global_option_in_poll_id": 5, + "content_object_id": null, "vote_ids": [], "meeting_id": 1 }], @@ -2405,7 +2458,7 @@ "mimetype": null, "pdf_information": {}, "create_timestamp": 1584513763, - "has_inherited_access_groups": true, + "is_public": false, "access_group_ids": [2, 3], "inherited_access_group_ids": [2, 3], @@ -2428,7 +2481,7 @@ "mimetype": "text/plain", "pdf_information": {}, "create_timestamp": 1584513771, - "has_inherited_access_groups": false, + "is_public": true, "access_group_ids": [], "inherited_access_group_ids": [], @@ -2451,7 +2504,7 @@ "mimetype": "image/png", "pdf_information": {}, "create_timestamp": 1584513791, - "has_inherited_access_groups": true, + "is_public": false, "access_group_ids": [], "inherited_access_group_ids": [2, 3], @@ -2513,8 +2566,8 @@ "show_title": true, "show_logo": true, - "current_projection_ids": [6], - "current_element_ids": ["clock/1"], + "current_projection_ids": [], + "current_element_ids": [], "preview_projection_ids": [], "history_projection_ids": [], "used_as_reference_projector_meeting_id": 1, @@ -2535,7 +2588,7 @@ "id": 2, "current_projector_id": null, "preview_projector_id": 1, - "history_projector_history_id": null, + "history_projector_id": null, "element_id": "motion/4", "options": { "mode": "diff" @@ -2550,17 +2603,6 @@ "element_id": "assignment/1", "options": {}, "meeting_id": 1 - }, - { - "id": 6, - "current_projector_id": 1, - "preview_projector_id": null, - "history_projector_id": null, - "element_id": "clock/1", - "options": { - "stable": true - }, - "meeting_id": 1 }], "projectiondefault": [ { @@ -2698,7 +2740,7 @@ "title": "Countdown 1", "description": "", "default_time": 60, - "countdown_time": 60.0, + "countdown_time": 60, "running": false, "projection_ids": [], diff --git a/docs/modelsvalidator/check_json.py b/docs/modelsvalidator/check_json.py deleted file mode 100644 index 238d2702b..000000000 --- a/docs/modelsvalidator/check_json.py +++ /dev/null @@ -1,100 +0,0 @@ -# This script requires fastjsonschema and pyyaml to be installed e. g. via pip. - -import json -import sys -from typing import Any, Dict, Iterable - -import fastjsonschema # type:ignore -import yaml - -MODELS_YML_PATH = "../../docs/models.yml" - -CHECKED_FILES = [ - "../../docker/initial-data.json", - "../../docs/example-data.json", -] - -SCHEMA = fastjsonschema.compile( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Schema for initial and example data.", - "type": "object", - "patternProperties": { - "^[a-z_]+$": { - "type": "array", - "items": { - "type": "object", - "properties": {"id": {"type": "number"}}, - "required": ["id"], - }, - } - }, - "additionalProperties": False, - } -) - - -class CheckException(Exception): - pass - - -def run_check(data: Dict) -> None: - try: - SCHEMA(data) - except fastjsonschema.exceptions.JsonSchemaException as e: - raise CheckException(f"JSON does not match schema: {str(e)}") - check_collections(data.keys()) - for collection, elements in data.items(): - for element in elements: - check_instance(collection, element) - - -def get_models() -> Dict[str, Any]: - with open(MODELS_YML_PATH, "rb") as x: - models_yml = x.read() - models_yml = models_yml.replace(" yes:".encode(), ' "yes":'.encode()) - models_yml = models_yml.replace(" no:".encode(), ' "no":'.encode()) - return yaml.safe_load(models_yml) - - -def check_collections(collections: Iterable[str]) -> None: - c1 = set(collections) - c2 = set(get_models().keys()) - if c1 != c2: - err = "Collections in JSON file do not match with models.yml." - if c2 - c1: - err += f" Missing collections: {', '.join(c2-c1)}." - if c1 - c2: - err += f" Invalid collections: {', '.join(c1-c2)}." - raise CheckException(err) - - -def check_instance(name: str, instance: Dict[str, Any]) -> None: - collection = get_models()[name] - for field_name in instance.keys(): - if "$" in field_name and not ("$_" in field_name or field_name[-1] == "$"): - # Structured field. - # TODO: Check this. - continue - if field_name not in collection.keys(): - raise CheckException(f"Bad field in {name}: {field_name}") - - -def main() -> int: - failed = False - for f in CHECKED_FILES: - with open(f) as data: - try: - run_check(json.load(data)) - except CheckException as e: - print(f"Check for {f} failed:", e) - failed = True - else: - print(f"Check for {f} successful.") - if failed: - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main())