From 99b0bc6e3b70103a4c08010886fb21cb4edcbc16 Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Wed, 11 Nov 2020 12:02:23 +0100 Subject: [PATCH] Restructure polls --- .github/workflows/models.yml | 13 +- .gitignore | 4 + docs/example-data.json | 684 ++++++++++-------- docs/models.yml | 433 ++++++----- docs/modelsvalidator/README.md | 20 + .../cmd/modelsvalidator/main.go | 55 ++ docs/modelsvalidator/go.mod | 7 + docs/modelsvalidator/go.sum | 3 + docs/modelsvalidator/models/check.go | 156 ++++ docs/modelsvalidator/models/check_test.go | 125 ++++ docs/modelsvalidator/models/error.go | 38 + docs/modelsvalidator/models/models.go | 233 ++++++ openslides-client | 2 +- 13 files changed, 1237 insertions(+), 536 deletions(-) create mode 100644 docs/modelsvalidator/README.md create mode 100644 docs/modelsvalidator/cmd/modelsvalidator/main.go create mode 100644 docs/modelsvalidator/go.mod create mode 100644 docs/modelsvalidator/go.sum create mode 100644 docs/modelsvalidator/models/check.go create mode 100644 docs/modelsvalidator/models/check_test.go create mode 100644 docs/modelsvalidator/models/error.go create mode 100644 docs/modelsvalidator/models/models.go diff --git a/.github/workflows/models.yml b/.github/workflows/models.yml index 5ba5828ee..92703ebb2 100644 --- a/.github/workflows/models.yml +++ b/.github/workflows/models.yml @@ -10,13 +10,14 @@ jobs: with: go-version: 1.15 - - name: Install validator - run: go get github.com/OpenSlides/openslides-modelsvalidate/cmd/modelsvalidate@v0.1.2 - env: - GO111MODULE: on - - name: Check out code uses: actions/checkout@v2 + - name: Build validator + run: go build ./cmd/modelsvalidator + working-directory: docs/modelsvalidator + env: + GO111MODULE: on + - name: Validate models.yml - run: $HOME/go/bin/modelsvalidate docs/models.yml \ No newline at end of file + run: docs/modelsvalidator/modelsvalidator docs/models.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 15f04f076..0326dc7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ .idea *.code-workspace +# docs +docs/modelsvalidator/modelsvalidator +dev-commands/export.json + # certs *.pem diff --git a/docs/example-data.json b/docs/example-data.json index 7b4b6d571..2d8bbfb33 100644 --- a/docs/example-data.json +++ b/docs/example-data.json @@ -53,18 +53,16 @@ "supported_motion_$_ids": [], "submitted_motion_$_ids": ["1"], "submitted_motion_$1_ids": [1, 2, 3, 4], - "motion_poll_voted_$_ids": [], - "motion_vote_$_ids": [], "assignment_candidate_$_ids": ["1"], "assignment_candidate_$1_ids": [1], - "assignment_poll_voted_$_ids": ["1"], - "assignment_poll_voted_$1_ids": [3], - "assignment_option_$_ids": ["1"], - "assignment_option_$1_ids": [1, 2], - "assignment_vote_$_ids": ["1"], - "assignment_vote_$1_ids": [4], - "assignment_delegated_vote_$_ids": ["1"], - "assignment_delegated_vote_$1_ids": [4] + "poll_voted_$_ids": ["1"], + "poll_voted_$1_ids": [5], + "option_$_ids": ["1"], + "option_$1_ids": [3, 4], + "vote_$_ids": ["1"], + "vote_$1_ids": [7], + "vote_delegated_vote_$_ids": ["1"], + "vote_delegated_vote_$1_ids": [7] }, { "id": 2, @@ -103,14 +101,13 @@ "personal_note_$_ids": [], "supported_motion_$_ids": [], "submitted_motion_$_ids": [], - "motion_poll_voted_$_ids": [], - "motion_vote_$_ids": [], + "poll_voted_$_ids": [], "assignment_candidate_$_ids": ["1"], "assignment_candidate_$1_ids": [3, 5], - "assignment_poll_voted_$_ids": [], - "assignment_option_$_ids": ["1"], - "assignment_option_$1_ids": [6], - "assignment_vote_$_ids": [] + "option_$_ids": ["1"], + "option_$1_ids": [6, 8], + "vote_$_ids": [], + "vote_delegated_vote_$_ids": [] }, { "id": 3, @@ -149,14 +146,13 @@ "supported_motion_$_ids": ["1"], "supported_motion_$1_ids": [3], "submitted_motion_$_ids": [], - "motion_poll_voted_$_ids": [], - "motion_vote_$_ids": [], + "poll_voted_$_ids": [], "assignment_candidate_$_ids": ["1"], "assignment_candidate_$1_ids": [2, 4], - "assignment_poll_voted_$_ids": [], - "assignment_option_$_ids": ["1"], - "assignment_option_$1_ids": [3, 4, 5], - "assignment_vote_$_ids": [] + "option_$_ids": ["1"], + "option_$1_ids": [5, 7], + "vote_$_ids": [], + "vote_delegated_vote_$_ids": [] }], "role": [ { @@ -288,6 +284,9 @@ "assignments_export_title": "Elections", "assignments_export_preamble": "", + + "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER", + "assignment_poll_ballot_paper_number": 8, "assignment_poll_add_candidates_to_list_of_speakers": true, "assignment_poll_sort_poll_result_by_votes": true, "assignment_poll_default_type": "nominal", @@ -296,25 +295,40 @@ "assignment_poll_default_majority_method": "simple", "assignment_poll_default_group_ids": [3, 5], + "poll_ballot_paper_selection": "CUSTOM_NUMBER", + "poll_ballot_paper_number": 8, + "poll_sort_poll_result_by_votes": true, + "poll_default_type": "nominal", + "poll_default_method": "votes", + "poll_default_100_percent_base": "valid", + "poll_default_majority_method": "simple", + "poll_default_group_ids": [3], + "projector_ids": [1, 2], - "projectiondefault_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + "projection_ids": [1, 2, 4, 6], + "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], "tag_ids": [1, 2, 3], "agenda_item_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "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], "mediafile_ids": [1, 2, 3], "motion_ids": [1, 2, 3, 4], + "motion_submitter_ids": [1, 2, 3, 4], "motion_comment_section_ids": [1], "motion_category_ids": [1, 2], "motion_block_ids": [1], "motion_workflow_ids": [1, 2], "motion_statute_paragraph_ids": [], - "motion_poll_ids": [1, 2], + "poll_ids": [1, 2, 3, 4, 5], + "option_ids": [1, 2, 3, 4, 5, 6, 7, 8], + "vote_ids": [1, 2, 3, 4, 5, 6, 7], "assignment_ids": [1, 2], - "assignment_poll_ids": [1, 2, 3], + "assignment_candidate_ids": [1, 2, 3, 4, 5], + "personal_note_ids": [1], "logo_$_id": ["web_header"], "logo_$web_header_id": 3, @@ -323,7 +337,7 @@ "committee_id": 1, "default_meeting_for_committee_id": 1, "present_user_ids": [1], - "temorary_user_ids": [], + "temporary_user_ids": [], "guest_ids": [3], "user_ids": [1, 2, 3], "reference_projector_id": 2, @@ -352,10 +366,10 @@ "mediafile_access_group_ids": [], "read_comment_section_ids": [], "write_comment_section_ids": [], - "motion_poll_ids": [], - "assignment_poll_ids": [], + "poll_ids": [], "used_as_motion_poll_default_id": null, "used_as_assignment_poll_default_id": null, + "used_as_poll_default_id": null, "meeting_id": 1 }, { @@ -370,10 +384,10 @@ "mediafile_inherited_access_group_ids": [1, 3], "read_comment_section_ids": [], "write_comment_section_ids": [], - "motion_poll_ids": [], - "assignment_poll_ids": [3], + "poll_ids": [5], "used_as_motion_poll_default_id": 1, "used_as_assignment_poll_default_id": null, + "used_as_poll_default_id": null, "meeting_id": 1 }, { @@ -409,10 +423,10 @@ "mediafile_inherited_access_group_ids": [1, 3], "read_comment_section_ids": [1], "write_comment_section_ids": [1], - "motion_poll_ids": [], - "assignment_poll_ids": [], + "poll_ids": [], "used_as_motion_poll_default_id": 1, "used_as_assignment_poll_default_id": 1, + "used_as_poll_default_id": 1, "meeting_id": 1 }, { @@ -437,10 +451,10 @@ "mediafile_access_group_ids": [], "read_comment_section_ids": [], "write_comment_section_ids": [], - "motion_poll_ids": [], - "assignment_poll_ids": [], + "poll_ids": [], "used_as_motion_poll_default_id": null, "used_as_assignment_poll_default_id": 1, + "used_as_poll_default_id": null, "meeting_id": 1 }, { @@ -469,10 +483,10 @@ "mediafile_access_group_ids": [], "read_comment_section_ids": [1], "write_comment_section_ids": [1], - "motion_poll_ids": [], - "assignment_poll_ids": [], + "poll_ids": [], "used_as_motion_poll_default_id": null, "used_as_assignment_poll_default_id": null, + "used_as_poll_default_id": null, "meeting_id": 1 }], "personal_note": [ @@ -517,7 +531,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 1000, + "weight": 2, "level": 0, "content_object_id": "topic/1", @@ -537,7 +551,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 1001, + "weight": 4, "level": 1, "content_object_id": "assignment/2", @@ -557,7 +571,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 1001, + "weight": 6, "level": 0, "content_object_id": "topic/2", @@ -577,7 +591,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 1002, + "weight": 8, "level": 0, "content_object_id": "topic/3", @@ -597,7 +611,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 1003, + "weight": 10, "level": 0, "content_object_id": "topic/4", @@ -617,7 +631,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 1004, + "weight": 12, "level": 0, "content_object_id": "topic/5", @@ -637,7 +651,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 1005, + "weight": 14, "level": 0, "content_object_id": "topic/6", @@ -657,7 +671,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 1006, + "weight": 16, "level": 0, "content_object_id": "topic/7", @@ -677,7 +691,7 @@ "is_internal": false, "is_hidden": true, "duration": null, - "weight": 1007, + "weight": 18, "level": 0, "content_object_id": "topic/8", @@ -697,7 +711,7 @@ "is_internal": false, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 20, "level": 0, "content_object_id": "motion/1", @@ -717,7 +731,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 22, "level": 0, "content_object_id": "motion/2", @@ -737,7 +751,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 24, "level": 0, "content_object_id": "assignment/1", @@ -757,7 +771,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 26, "level": 0, "content_object_id": "motion/3", @@ -777,7 +791,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 28, "level": 0, "content_object_id": "motion/4", @@ -797,7 +811,7 @@ "is_internal": true, "is_hidden": false, "duration": null, - "weight": 10000, + "weight": 30, "level": 0, "content_object_id": "motion_block/1", @@ -1255,6 +1269,8 @@ "submitter_ids": [1], "supporter_ids": [], "poll_ids": [1, 2], + "option_$_ids": ["1"], + "option_$1_ids": [1, 2], "change_recommendation_ids": [], "statute_paragraph_id": null, "comment_ids": [1], @@ -1299,6 +1315,7 @@ "submitter_ids": [2], "supporter_ids": [], "poll_ids": [], + "option_$_ids": [], "change_recommendation_ids": [], "statute_paragraph_id": null, "comment_ids": [], @@ -1343,6 +1360,7 @@ "submitter_ids": [3], "supporter_ids": [3], "poll_ids": [], + "option_$_ids": [], "change_recommendation_ids": [5], "statute_paragraph_id": null, "comment_ids": [], @@ -1387,6 +1405,7 @@ "submitter_ids": [4], "supporter_ids": [], "poll_ids": [], + "option_$_ids": [], "change_recommendation_ids": [4], "statute_paragraph_id": null, "comments": [], @@ -1538,9 +1557,9 @@ "previous_state_ids": [], "motion_ids": [1, 2, 3], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 1, - "first_state_of_workflow_id": 1 + "first_state_of_workflow_id": 1, + "meeting_id": 1 }, { "id": 2, @@ -1560,9 +1579,9 @@ "previous_state_ids": [1], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 1, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 3, @@ -1582,9 +1601,9 @@ "previous_state_ids": [1], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 1, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 4, @@ -1604,9 +1623,9 @@ "previous_state_ids": [1], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 1, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 5, @@ -1626,9 +1645,9 @@ "previous_state_ids": [], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": 2 + "first_state_of_workflow_id": 2, + "meeting_id": 1 }, { "id": 6, @@ -1648,9 +1667,9 @@ "previous_state_ids": [5], "motion_ids": [4], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 7, @@ -1670,9 +1689,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 8, @@ -1692,9 +1711,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 9, @@ -1714,9 +1733,9 @@ "previous_state_ids": [5, 6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 10, @@ -1736,9 +1755,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 11, @@ -1758,9 +1777,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 12, @@ -1780,9 +1799,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 13, @@ -1802,9 +1821,9 @@ "previous_state_ids": [6], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }, { "id": 14, @@ -1824,9 +1843,9 @@ "previous_state_ids": [5], "motion_ids": [], "motion_recommendation_ids": [], - "meeting_id": 1, "workflow_id": 2, - "first_state_of_workflow_id": null + "first_state_of_workflow_id": null, + "meeting_id": 1 }], "motion_workflow": [ { @@ -1852,21 +1871,31 @@ "meeting_id": 1 }], "motion_statute_paragraph": [], -"motion_poll": [ +"poll": [ { "id": 1, + "title": "1", + "description": "", + "type": "analog", "pollmethod": "YNA", "state": 3, - "type": "analog", - "title": "1", + "min_votes_amount": 1, + "max_votes_amount": 1, + "allow_multiple_votes_per_candidate": false, + "global_yes": false, + "global_no": false, + "global_abstain": false, "onehundred_percent_base": "YNA", "majority_method": "simple", + "amount_global_yes": null, + "amount_global_no": null, + "amount_global_abstain": null, "votesvalid": "2.000000", "votesinvalid": "9.000000", "votescast": "2.000000", "user_has_voted": false, - "motion_id": 1, + "content_object_id": "motion/1", "option_ids": [1], "voted_ids": [], "entitled_group_ids": [], @@ -1876,45 +1905,226 @@ }, { "id": 2, + "title": "2", + "description": "", + "type": "analog", "pollmethod": "YNA", "state": 1, - "type": "analog", - "title": "2", + "min_votes_amount": 1, + "max_votes_amount": 1, + "allow_multiple_votes_per_candidate": false, + "global_yes": false, + "global_no": false, + "global_abstain": false, "onehundred_percent_base": "YNA", "majority_method": "simple", + "amount_global_yes": null, + "amount_global_no": null, + "amount_global_abstain": null, "votesvalid": null, "votesinvalid": null, "votescast": null, "user_has_voted": false, - "motion_id": 1, + "content_object_id": "motion/1", "option_ids": [2], "voted_ids": [], "entitled_group_ids": [], "projection_ids": [], "current_projector_ids": [], "meeting_id": 1 + }, + { + "id": 3, + "title": "1", + "description": "", + "type": "analog", + "pollmethod": "YNA", + "state": 1, + "min_votes_amount": 1, + "max_votes_amount": 1, + "allow_multiple_votes_per_candidate": false, + "global_yes": false, + "global_no": true, + "global_abstain": true, + "onehundred_percent_base": "YNA", + "majority_method": "simple", + "amount_global_yes": null, + "amount_global_no": null, + "amount_global_abstain": null, + "votesvalid": null, + "votesinvalid": null, + "votescast": null, + "user_has_voted": false, + + "content_object_id": "assignment/1", + "voted_ids": [], + "entitled_group_ids": [], + "option_ids": [3], + "projection_ids": [], + "current_projector_ids": [], + "meeting_id": 1 + }, + { + "id": 4, + "title": "2", + "description": "", + "type": "analog", + "pollmethod": "Y", + "state": 3, + "min_votes_amount": 1, + "max_votes_amount": 1, + "allow_multiple_votes_per_candidate": false, + "global_yes": false, + "global_no": true, + "global_abstain": true, + "onehundred_percent_base": "Y", + "majority_method": "simple", + "amount_global_yes": null, + "amount_global_no": "2.000000", + "amount_global_abstain": "1.000000", + "votesvalid": "9.000000", + "votesinvalid": "2.000000", + "votescast": "16.000000", + "user_has_voted": false, + + "content_object_id": "assignment/1", + "voted_ids": [], + "entitled_group_ids": [], + "option_ids": [4, 5, 6], + "projection_ids": [], + "current_projector_ids": [], + "meeting_id": 1 + }, + { + "id": 5, + "title": "Wahlgang", + "description": "", + "type": "named", + "pollmethod": "Y", + "state": 3, + "min_votes_amount": 1, + "max_votes_amount": 1, + "allow_multiple_votes_per_candidate": false, + "global_yes": false, + "global_no": true, + "global_abstain": false, + "onehundred_percent_base": "valid", + "majority_method": "simple", + "amount_global_yes": null, + "amount_global_no": "0.000000", + "amount_global_abstain": null, + "votesvalid": "1.000000", + "votesinvalid": "0.000000", + "votescast": "1.000000", + "user_has_voted": true, + + "content_object_id": "assignment/2", + "voted_ids": [1], + "entitled_group_ids": [2], + "option_ids": [7, 8], + "projection_ids": [], + "current_projector_ids": [], + "meeting_id": 1 }], -"motion_option": [ +"option": [ { "id": 1, "yes": "2.000000", "no": "4.000000", "abstain": "1.000000", + "weight": 10000, "poll_id": 1, - "vote_ids": [1, 2, 3] + "content_object_id": "motion/1", + "vote_ids": [1, 2, 3], + "meeting_id": 1 }, { "id": 2, "yes": "0.000000", "no": "0.000000", "abstain": "0.000000", + "weight": 10000, "poll_id": 2, - "vote_ids": [] + "content_object_id": "motion/1", + "vote_ids": [], + "meeting_id": 1 + }, + { + "id": 3, + "yes": "0.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 1, + + "poll_id": 3, + "content_object_id": "user/1", + "vote_ids": [], + "meeting_id": 1 + }, + { + "id": 4, + "yes": "3.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 1, + + "poll_id": 4, + "content_object_id": "user/1", + "vote_ids": [1], + "meeting_id": 1 + }, + { + "id": 5, + "yes": "7.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 2, + + "poll_id": 4, + "content_object_id": "user/3", + "vote_ids": [3], + "meeting_id": 1 + }, + { + "id": 6, + "yes": "2.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 3, + + "poll_id": 4, + "content_object_id": "user/2", + "vote_ids": [2], + "meeting_id": 1 + }, + { + "id": 7, + "yes": "0.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 1, + + "poll_id": 5, + "content_object_id": "user/3", + "vote_ids": [], + "meeting_id": 1 + }, + { + "id": 8, + "yes": "1.000000", + "no": "0.000000", + "abstain": "0.000000", + "weight": 2, + + "poll_id": 5, + "content_object_id": "user/2", + "vote_ids": [4], + "meeting_id": 1 }], -"motion_vote": [ +"vote": [ { "id": 1, "weight": "2.000000", @@ -1922,7 +2132,8 @@ "user_id": null, "delegated_user_id": null, - "option_id": 1 + "option_id": 1, + "meeting_id": 1 }, { "id": 2, @@ -1931,7 +2142,8 @@ "user_id": null, "delegated_user_id": null, - "option_id": 1 + "option_id": 1, + "meeting_id": 1 }, { "id": 3, @@ -1940,7 +2152,48 @@ "user_id": null, "delegated_user_id": null, - "option_id": 1 + "option_id": 1, + "meeting_id": 1 + }, + { + "id": 4, + "value": "Y", + "weight": "3.000000", + + "user_id": null, + "delegated_user_id": null, + "option_id": 4, + "meeting_id": 1 + }, + { + "id": 5, + "value": "Y", + "weight": "2.000000", + + "user_id": null, + "delegated_user_id": null, + "option_id": 6, + "meeting_id": 1 + }, + { + "id": 6, + "value": "Y", + "weight": "7.000000", + + "user_id": null, + "delegated_user_id": null, + "option_id": 5, + "meeting_id": 1 + }, + { + "id": 7, + "value": "Y", + "weight": "1.000000", + + "user_id": 1, + "delegated_user_id": 1, + "option_id": 8, + "meeting_id": 1 }], "assignment": [ { @@ -1953,7 +2206,8 @@ "number_poll_candidates": false, "candidate_ids": [1, 2, 3], - "poll_ids": [1, 2], + "poll_ids": [3, 4], + "option_$_ids": [], "agenda_item_id": 11, "list_of_speakers_id": 11, "tag_ids": [], @@ -1972,7 +2226,8 @@ "number_poll_candidates": true, "candidate_ids": [4, 5], - "poll_ids": [3], + "poll_ids": [5], + "option_$_ids": [], "agenda_item_id": 14, "list_of_speakers_id": 14, "tag_ids": [2], @@ -1987,224 +2242,40 @@ "weight": 1, "assignment_id": 1, - "user_id": 1 + "user_id": 1, + "meeting_id": 1 }, { "id": 2, "weight": 2, "assignment_id": 1, - "user_id": 3 + "user_id": 3, + "meeting_id": 1 }, { "id": 3, "weight": 3, "assignment_id": 1, - "user_id": 2 + "user_id": 2, + "meeting_id": 1 }, { "id": 4, "weight": 1, "assignment_id": 2, - "user_id": 3 + "user_id": 3, + "meeting_id": 1 }, { "id": 5, "weight": 2, "assignment_id": 2, - "user_id": 2 - }], -"assignment_poll": [ - { - "id": 1, - "description": "", - "pollmethod": "YNA", - "votes_amount": 1, - "allow_multiple_votes_per_candidate": false, - "global_no": true, - "global_abstain": true, - "amount_global_no": null, - "amount_global_abstain": null, - "state": 1, - "title": "1", - "type": "analog", - "onehundred_percent_base": "YNA", - "majority_method": "simple", - "votesvalid": null, - "votesinvalid": null, - "votescast": null, - "user_has_voted": false, - - "assignment_id": 1, - "voted_ids": [], - "entitled_group_ids": [], - "option_ids": [1], - "projection_ids": [], - "current_projector_ids": [], - "meeting_id": 1 - }, - { - "id": 2, - "description": "", - "pollmethod": "votes", - "votes_amount": 1, - "allow_multiple_votes_per_candidate": false, - "global_no": true, - "global_abstain": true, - "amount_global_no": "2.000000", - "amount_global_abstain": "1.000000", - "state": 3, - "title": "2", - "type": "analog", - "onehundred_percent_base": "votes", - "majority_method": "simple", - "votesvalid": "9.000000", - "votesinvalid": "2.000000", - "votescast": "16.000000", - "user_has_voted": false, - - "assignment_id": 1, - "voted_ids": [], - "entitled_group_ids": [], - "option_ids": [2, 3, 4], - "projection_ids": [], - "current_projector_ids": [], - "meeting_id": 1 - }, - { - "id": 3, - "description": "", - "pollmethod": "votes", - "votes_amount": 1, - "allow_multiple_votes_per_candidate": false, - "global_no": true, - "global_abstain": false, - "amount_global_no": "0.000000", - "amount_global_abstain": null, - "state": 3, - "title": "Wahlgang", - "type": "named", - "onehundred_percent_base": "valid", - "majority_method": "simple", - "votesvalid": "1.000000", - "votesinvalid": "0.000000", - "votescast": "1.000000", - "user_has_voted": true, - - "assignment_id": 2, - "voted_ids": [1], - "entitled_group_ids": [2], - "option_ids": [5, 6], - "projection_ids": [], - "current_projector_ids": [], - "meeting_id": 1 - }], -"assignment_option": [ - { - "id": 1, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - - "poll_id": 1, - "user_id": 1, - "vote_ids": [] - }, - { - "id": 2, - "yes": "3.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - - "poll_id": 2, - "user_id": 1, - "vote_ids": [1] - }, - { - "id": 3, - "yes": "7.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 2, - - "poll_id": 2, - "user_id": 3, - "vote_ids": [3] - }, - { - "id": 4, - "yes": "2.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 3, - - "poll_id": 2, "user_id": 2, - "vote_ids": [2] - }, - { - "id": 5, - "yes": "0.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 1, - - "poll_id": 3, - "user_id": 3, - "vote_ids": [] - }, - { - "id": 6, - "yes": "1.000000", - "no": "0.000000", - "abstain": "0.000000", - "weight": 2, - - "poll_id": 3, - "user_id": 2, - "vote_ids": [4] - }], -"assignment_vote": [ - { - "id": 1, - "value": "Y", - "weight": "3.000000", - - "user_id": null, - "delegated_user_id": null, - "option_id": 2 - }, - { - "id": 2, - "value": "Y", - "weight": "2.000000", - - "user_id": null, - "delegated_user_id": null, - "option_id": 4 - }, - { - "id": 3, - "value": "Y", - "weight": "7.000000", - - "user_id": null, - "delegated_user_id": null, - "option_id": 3 - }, - { - "id": 4, - "value": "Y", - "weight": "1.000000", - - "user_id": 1, - "delegated_user_id": 1, - "option_id": 6 + "meeting_id": 1 }], "mediafile": [ { @@ -2226,10 +2297,9 @@ "projection_ids": [], "current_projector_ids": [], "attachment_ids": [], - "meeting_id": 1, - "used_as_logo_$_in_meeting_id": [], - "used_as_font_$_in_meeting_id": [] + "used_as_font_$_in_meeting_id": [], + "meeting_id": 1 }, { "id": 2, @@ -2250,10 +2320,9 @@ "projection_ids": [], "current_projector_ids": [], "attachment_ids": ["motion/4"], - "meeting_id": 1, - "used_as_logo_$_in_meeting_id": [], - "used_as_font_$_in_meeting_id": [] + "used_as_font_$_in_meeting_id": [], + "meeting_id": 1 }, { "id": 3, @@ -2274,11 +2343,10 @@ "projection_ids": [], "current_projector_ids": [], "attachment_ids": [], - "meeting_id": 1, - "used_as_logo_$_in_meeting_id": ["web_header"], "used_as_logo_$web_header_in_meeting_id": 1, - "used_as_font_$_in_meeting_id": [] + "used_as_font_$_in_meeting_id": [], + "meeting_id": 1 }], "projector": [ { @@ -2305,7 +2373,7 @@ "preview_projection_ids": [1, 2], "history_projection_ids": [], "used_as_reference_projector_meeting_id": null, - "projectiondefault_ids": [1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + "projectiondefault_ids": [1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "meeting_id": 1 }, { @@ -2342,7 +2410,8 @@ "preview_projector_id": 1, "history_projector_id": null, "element_id": "motion_block/1", - "options": {} + "options": {}, + "meeting_id": 1 }, { "id": 2, @@ -2352,7 +2421,8 @@ "element_id": "motion/4", "options": { "mode": "diff" - } + }, + "meeting_id": 1 }, { "id": 4, @@ -2360,7 +2430,8 @@ "preview_projector_id": null, "history_projector_id": null, "element_id": "assignment/1", - "options": {} + "options": {}, + "meeting_id": 1 }, { "id": 6, @@ -2370,7 +2441,8 @@ "element_id": "clock/1", "options": { "stable": true - } + }, + "meeting_id": 1 }], "projectiondefault": [ { @@ -2479,6 +2551,14 @@ }, { "id": 14, + "name": "poll", + "display_name": "Poll", + + "projector_id": 1, + "meeting_id": 1 + }, + { + "id": 15, "name": "amendments", "display_name": "Amendments", diff --git a/docs/models.yml b/docs/models.yml index 527ecf813..5198da1e4 100644 --- a/docs/models.yml +++ b/docs/models.yml @@ -23,8 +23,19 @@ # `motion/category_id`. The type indicates that there are many # motion ids. # - Generic relations: The difference to non-generic relations is that you have a -# list of possible collections. Therefor we split the simple notation up to the -# properties `collection` and `field`. +# list of possible collections. Therefore we split the simple notation up to the +# properties `collections` and `field`, if each collection has the same field. +# If the different collections have different fields, you can give multiple +# `collection` and `field`. E.g.: +# to: +# collections: +# - collection: motion +# field: option_ids +# - collection: user +# field: +# name: option_$_ids +# type: structured-relation +# replacement: meeting_id # - on_delete: This fields determines what should happen with the foreign model if # this model gets deleted. Possible values are: # - SET_NULL (default): delete the id from the foreign key @@ -173,54 +184,36 @@ user: fields: type: relation-list to: motion_submitter/user_id - motion_poll_voted_$_ids: + poll_voted_$_ids: type: template replacement: meeting_id fields: type: relation-list - to: motion_poll/voted_ids - motion_vote_$_ids: + to: poll/voted_ids + option_$_ids: type: template replacement: meeting_id fields: type: relation-list - to: motion_vote/user_id - motion_delegated_vote_$_ids: + to: option/content_object_id + vote_$_ids: type: template replacement: meeting_id fields: type: relation-list - to: motion_vote/delegated_user_id + to: vote/user_id + vote_delegated_vote_$_ids: + type: template + replacement: meeting_id + fields: + type: relation-list + to: vote/delegated_user_id assignment_candidate_$_ids: type: template replacement: meeting_id fields: type: relation-list to: assignment_candidate/user_id - assignment_poll_voted_$_ids: - type: template - replacement: meeting_id - fields: - type: relation-list - to: assignment_poll/voted_ids - assignment_option_$_ids: - type: template - replacement: meeting_id - fields: - type: relation-list - to: assignment_option/user_id - assignment_vote_$_ids: - type: template - replacement: meeting_id - fields: - type: relation-list - to: assignment_vote/user_id - assignment_delegated_vote_$_ids: - type: template - replacement: meeting_id - fields: - type: relation-list - to: assignment_vote/delegated_user_id vote_delegated_$_to_id: type: template replacement: meeting_id @@ -487,6 +480,7 @@ meeting: motions_export_submitter_recommendation: boolean motions_export_follow_recommendation: boolean + # Motion poll motion_poll_ballot_paper_selection: type: string enum: @@ -532,6 +526,7 @@ meeting: assignemnts_export_title: string assignments_export_preamble: string + # Assignment polls assignment_poll_ballot_paper_selection: type: string enum: @@ -549,10 +544,32 @@ meeting: type: relation-list to: group/used_as_assignment_poll_default_id + # Polls + poll_ballot_paper_selection: + type: string + enum: + - NUMBER_OF_DELEGATES + - NUMBER_OF_ALL_PARTICIPANTS + - CUSTOM_NUMBER + poll_ballot_paper_number: number + poll_sort_poll_result_by_votes: boolean + poll_default_type: string + poll_default_method: string + poll_default_100_percent_base: string + poll_default_majority_method: string + poll_default_group_ids: + type: relation-list + to: group/used_as_poll_default_id + + # Relations projector_ids: type: relation-list to: projector/meeting_id on_delete: CASCADE + projection_ids: + type: relation-list + to: projection/meeting_id + on_delete: CASCADE projectiondefault_ids: type: relation-list to: projectiondefault/meeting_id @@ -577,6 +594,10 @@ meeting: type: relation-list to: list_of_speakers/meeting_id on_delete: CASCADE + speaker_ids: + type: relation-list + to: speaker/meeting_id + on_delete: CASCADE topic_ids: type: relation-list to: topic/meeting_id @@ -613,38 +634,6 @@ meeting: type: relation-list to: motion_statute_paragraph/meeting_id on_delete: CASCADE - motion_poll_ids: - type: relation-list - to: motion_poll/meeting_id - on_delete: CASCADE - assignment_ids: - type: relation-list - to: assignment/meeting_id - on_delete: CASCADE - assignment_poll_ids: - type: relation-list - to: assignment_poll/meeting_id - on_delete: CASCADE - personal_note_ids: - type: relation-list - to: personal_note/meeting_id - on_delete: CASCADE - projection_ids: - type: relation-list - to: projection/meeting_id - on_delete: CASCADE - speaker_ids: - type: relation-list - to: speaker/meeting_id - on_delete: CASCADE - motion_option_ids: - type: relation-list - to: motion_option/meeting_id - on_delete: CASCADE - motion_vote_ids: - type: relation-list - to: motion_vote/meeting_id - on_delete: CASCADE motion_comment_ids: type: relation-list to: motion_comment/meeting_id @@ -661,17 +650,29 @@ meeting: type: relation-list to: motion_state/meeting_id on_delete: CASCADE + poll_ids: + type: relation-list + to: poll/meeting_id + on_delete: CASCADE + option_ids: + type: relation-list + to: option/meeting_id + on_delete: CASCADE + vote_ids: + type: relation-list + to: vote/meeting_id + on_delete: CASCADE + assignment_ids: + type: relation-list + to: assignment/meeting_id + on_delete: CASCADE assignment_candidate_ids: type: relation-list to: assignment_candidate/meeting_id on_delete: CASCADE - assignment_option_ids: + personal_note_ids: type: relation-list - to: assignment_option/meeting_id - on_delete: CASCADE - assignment_vote_ids: - type: relation-list - to: assignment_vote/meeting_id + to: personal_note/meeting_id on_delete: CASCADE # Logos and Fonts @@ -778,13 +779,9 @@ group: type: relation-list to: motion_comment_section/write_group_ids equal_fields: meeting_id - motion_poll_ids: + poll_ids: type: relation-list - to: motion_poll/entitled_group_ids - equal_fields: meeting_id - assignment_poll_ids: - type: relation-list - to: assignment_poll/entitled_group_ids + to: poll/entitled_group_ids equal_fields: meeting_id used_as_motion_poll_default_id: type: relation @@ -792,6 +789,9 @@ group: used_as_assignment_poll_default_id: type: relation to: meeting/assignment_poll_default_group_ids + used_as_poll_default_id: + type: relation + to: meeting/poll_default_group_ids meeting_id: type: relation to: meeting/group_ids @@ -813,7 +813,7 @@ personal_note: content_object_id: type: generic-relation to: - collection: + collections: - motion field: personal_note_ids equal_fields: meeting_id @@ -831,7 +831,7 @@ tag: tagged_ids: type: generic-relation-list to: - collection: + collections: - agenda_item - assignment - motion @@ -878,7 +878,7 @@ agenda_item: content_object_id: type: generic-relation to: - collection: + collections: - motion - motion_block - assignment @@ -918,7 +918,7 @@ list_of_speakers: content_object_id: type: generic-relation to: - collection: + collections: - motion - motion_block - assignment @@ -1000,6 +1000,11 @@ topic: required: true on_delete: CASCADE equal_fields: meeting_id + option_ids: + type: relation-list + to: option/content_object_id + on_delete: CASCADE + equal_fields: meeting_id tag_ids: type: relation-list to: tag/tagged_ids @@ -1084,7 +1089,7 @@ motion: recommendation_extension_reference_ids: type: generic-relation-list to: - collection: + collections: - motion field: referenced_in_motion_recommendation_extension_ids equal_fields: meeting_id @@ -1116,7 +1121,12 @@ motion: equal_fields: meeting_id poll_ids: type: relation-list - to: motion_poll/motion_id + to: poll/content_object_id + on_delete: CASCADE + equal_fields: meeting_id + option_ids: + type: relation-list + to: option/content_object_id on_delete: CASCADE equal_fields: meeting_id change_recommendation_ids: @@ -1463,26 +1473,91 @@ motion_statute_paragraph: to: meeting/motion_statute_paragraph_ids required: true -motion_poll: +poll: id: number - pollmethod: string - state: number - type: string - title: string - onehundred_percent_base: string - majority_method: string + description: string + title: + type: string + required: true + type: + type: string + required: true + enum: + - analog + - named + - pseudoanonymous + pollmethod: + type: string + required: true + enum: + - Y + - YN + - YNA + - N + state: + type: number + default: 1 + enum: + - 1 + - 2 + - 3 + - 4 + min_votes_amount: + type: number + default: 1 + max_votes_amount: + type: number + default: 1 + allow_multiple_votes_per_candidate: + type: boolean + default: false + global_yes: + type: boolean + default: false + global_no: + type: boolean + default: false + global_abstain: + type: boolean + default: false + onehundred_percent_base: + type: string + required: true + enum: + - Y + - YN + - YNA + - valid + - cast + - disabled + majority_method: + type: string + required: true + enum: + - simple + - two_thirds + - three_quarters + - disabled + amount_global_yes: decimal(6) + amount_global_no: decimal(6) + amount_global_abstain: decimal(6) votesvalid: decimal(6) votesinvalid: decimal(6) votescast: decimal(6) user_has_voted: boolean # This is user specific and set during restriction + user_has_voted_for_delegations: number[] # This is user specific and set during restriction - motion_id: - type: relation - to: motion/poll_ids + content_object_id: # Note: must not be set - it is allowed to have standalone polls + type: generic-relation + to: + collections: + - motion + - assignment + field: poll_ids equal_fields: meeting_id option_ids: type: relation-list - to: motion_option/poll_id + to: option/poll_id on_delete: CASCADE equal_fields: meeting_id voted_ids: @@ -1490,12 +1565,12 @@ motion_poll: to: collection: user field: - name: motion_poll_voted_$_ids + name: poll_voted_$_ids type: structured-relation replacement: meeting_id entitled_group_ids: type: relation-list - to: group/motion_poll_ids + to: group/poll_ids equal_fields: meeting_id projection_ids: type: relation-list @@ -1507,43 +1582,63 @@ motion_poll: equal_fields: meeting_id meeting_id: type: relation - to: meeting/motion_poll_ids + to: meeting/poll_ids -motion_option: +option: id: number + weight: + type: number + default: 10000 + text: HTMLStrict yes: decimal(6) no: decimal(6) abstain: decimal(6) poll_id: type: relation - to: motion_poll/option_ids + to: poll/option_ids equal_fields: meeting_id + required: true vote_ids: type: relation-list - to: motion_vote/option_id + to: vote/option_id on_delete: CASCADE equal_fields: meeting_id + content_object_id: + type: generic-relation + to: + collections: # Now, we have multiple models to vote about. + - collection: motion + field: option_ids + - collection: topic + field: option_ids + - collection: user + field: + name: option_$_ids + type: structured-relation + replacement: meeting_id + equal_fields: meeting_id meeting_id: type: relation - to: meeting/motion_option_ids + to: meeting/option_ids required: true -motion_vote: +vote: id: number weight: decimal(6) value: string option_id: type: relation - to: motion_option/vote_ids + to: option/vote_ids equal_fields: meeting_id + required: true user_id: type: relation to: collection: user field: - name: motion_vote_$_ids + name: vote_$_ids type: structured-relation replacement: meeting_id delegated_user_id: @@ -1551,12 +1646,12 @@ motion_vote: to: collection: user field: - name: motion_delegated_vote_$_ids + name: vote_delegated_vote_$_ids type: structured-relation replacement: meeting_id meeting_id: type: relation - to: meeting/motion_vote_ids + to: meeting/vote_ids required: true assignment: @@ -1586,7 +1681,7 @@ assignment: equal_fields: meeting_id poll_ids: type: relation-list - to: assignment_poll/assignment_id + to: poll/content_object_id on_delete: CASCADE equal_fields: meeting_id agenda_item_id: @@ -1644,120 +1739,6 @@ assignment_candidate: to: meeting/assignment_candidate_ids required: true -assignment_poll: - id: number - description: string - pollmethod: string - votes_amount: number - allow_multiple_votes_per_candidate: boolean - global_abstain: boolean - global_no: boolean - amount_global_abstain: decimal(6) - amount_global_no: decimal(6) - state: number - title: string - type: string - onehundred_percent_base: string - majority_method: string - votescast: decimal(6) - votesinvalid: decimal(6) - votesvalid: decimal(6) - user_has_voted: boolean # This is user specific and set during restriction - - assignment_id: - type: relation - to: assignment/poll_ids - equal_fields: meeting_id - voted_ids: - type: relation-list - to: - collection: user - field: - name: assignment_poll_voted_$_ids - type: structured-relation - replacement: meeting_id - entitled_group_ids: - type: relation-list - to: group/assignment_poll_ids - equal_fields: meeting_id - option_ids: - type: relation-list - to: assignment_option/poll_id - on_delete: CASCADE - equal_fields: meeting_id - projection_ids: - type: relation-list - to: projection/element_id - equal_fields: meeting_id - current_projector_ids: - type: relation-list - to: projector/current_element_ids - equal_fields: meeting_id - meeting_id: - type: relation - to: meeting/assignment_poll_ids - -assignment_option: - id: number - yes: decimal(6) - no: decimal(6) - abstain: decimal(6) - weight: - type: number - default: 10000 - - poll_id: - type: relation - to: assignment_poll/option_ids - equal_fields: meeting_id - user_id: - type: relation - to: - collection: user - field: - name: assignment_option_$_ids - type: structured-relation - replacement: meeting_id - vote_ids: - type: relation-list - to: assignment_vote/option_id - on_delete: CASCADE - equal_fields: meeting_id - meeting_id: - type: relation - to: meeting/assignment_option_ids - required: true - -assignment_vote: - id: number - value: string - weight: decimal(6) - - option_id: - type: relation - to: assignment_option/vote_ids - equal_fields: meeting_id - user_id: - type: relation - to: - collection: user - field: - name: assignment_vote_$_ids - type: structured-relation - replacement: meeting_id - delegated_user_id: - type: relation - to: - collection: user - field: - name: assignment_delegated_vote_$_ids - type: structured-relation - replacement: meeting_id - meeting_id: - type: relation - to: meeting/assignment_vote_ids - required: true - # Mediafiles are delivered by the mediafile server with the URL # `/media//path` mediafile: @@ -1816,7 +1797,7 @@ mediafile: attachment_ids: type: generic-relation-list to: - collection: + collections: - motion - topic - assignment @@ -1880,7 +1861,7 @@ projector: current_element_ids: type: generic-relation-list to: - collection: + collections: - motion - mediafile - list_of_speakers @@ -1889,8 +1870,7 @@ projector: - agenda_item - topic - user - - assignment_poll - - motion_poll + - poll - projector_message - projector_countdown field: current_projector_ids @@ -1939,7 +1919,7 @@ projection: element_id: type: generic-relation to: - collection: + collections: - motion - mediafile - list_of_speakers @@ -1948,8 +1928,7 @@ projection: - agenda_item - topic - user - - assignment_poll - - motion_poll + - poll - projector_message - projector_countdown field: projection_ids diff --git a/docs/modelsvalidator/README.md b/docs/modelsvalidator/README.md new file mode 100644 index 000000000..0dbde2c98 --- /dev/null +++ b/docs/modelsvalidator/README.md @@ -0,0 +1,20 @@ +# Modelsvalidate + +Modelsvalidate is a tool to validate the models.yml file, that is used in the +development process of OpenSlides 4. + + +## Run + +The tool requires the content of the models.yml. It can be provided via stdin, a +file system path or an url starting with http:// or https://. + + +``` +cat models.yaml | modelstool +modelstool openslides/docs/models.yml +modelstool https://raw.githubusercontent.com/OpenSlides/OpenSlides/openslides4-dev/docs/models.yml +``` + +The tool returns with status code 0 and no content, if the given content is +valid. It returns with a positive status code and some error messages if not. \ No newline at end of file diff --git a/docs/modelsvalidator/cmd/modelsvalidator/main.go b/docs/modelsvalidator/cmd/modelsvalidator/main.go new file mode 100644 index 000000000..87eabf464 --- /dev/null +++ b/docs/modelsvalidator/cmd/modelsvalidator/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + + "github.com/OpenSlides/Openslides/modelsvalidator/models" +) + +func main() { + var content io.Reader = os.Stdin + if len(os.Args) > 1 { + c, err := openModels(os.Args[1]) + if err != nil { + log.Fatalf("Can not load content: %v", err) + } + defer c.Close() + content = c + } + + data, err := models.Unmarshal(content) + if err != nil { + log.Fatalf("Invalid model format: %v", err) + } + + if err := models.Check(data); err != nil { + log.Fatalf("Invalid model structure:\n\n%v", err) + } +} + +// openModels reads the model either from file or from an url. +func openModels(path string) (io.ReadCloser, error) { + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { + return openModelsFromURL(path) + } + + return os.Open(path) +} + +func openModelsFromURL(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("requesting models from url: %w", err) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("can not get models from url. Got status %s", resp.Status) + } + + return resp.Body, nil +} diff --git a/docs/modelsvalidator/go.mod b/docs/modelsvalidator/go.mod new file mode 100644 index 000000000..f4b49fdda --- /dev/null +++ b/docs/modelsvalidator/go.mod @@ -0,0 +1,7 @@ +module github.com/OpenSlides/Openslides/modelsvalidator + +go 1.15 + +require ( + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 +) diff --git a/docs/modelsvalidator/go.sum b/docs/modelsvalidator/go.sum new file mode 100644 index 000000000..76bb3135b --- /dev/null +++ b/docs/modelsvalidator/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/docs/modelsvalidator/models/check.go b/docs/modelsvalidator/models/check.go new file mode 100644 index 000000000..47b480a70 --- /dev/null +++ b/docs/modelsvalidator/models/check.go @@ -0,0 +1,156 @@ +package models + +import ( + "fmt" + "strings" +) + +// Check runs some checks on the given models. +func Check(models map[string]Model) error { + validators := []func(map[string]Model) error{ + validateTypes, + validateRelations, + validateTemplatePrefixes, + } + + errors := new(ErrorList) + for _, v := range validators { + if err := v(models); err != nil { + errors.append(err) + } + } + + if !errors.empty() { + return errors + } + return nil +} + +func validateTypes(models map[string]Model) error { + scalar := scalarTypes() + relation := relationTypes() + errs := &ErrorList{ + Name: "type validator", + intent: 1, + } + for modelName, model := range models { + for fieldName, field := range model.Fields { + if scalar[strings.TrimSuffix(field.Type, "[]")] { + continue + } + + if relation[field.Type] { + continue + } + + errs.append(fmt.Errorf("Unknown type `%s` in %s/%s", field.Type, modelName, fieldName)) + } + } + if errs.empty() { + return nil + } + return errs +} + +func validateRelations(models map[string]Model) error { + errs := &ErrorList{ + Name: "relation validator", + intent: 1, + } + relation := relationTypes() + for modelName, model := range models { + Next: + for fieldName, field := range model.Fields { + r := field.Relation() + if r == nil { + continue + } + + for _, c := range r.ToCollections() { + toModel, ok := models[c.Collection] + if !ok { + errs.append(fmt.Errorf("%s/%s directs to nonexisting model `%s`", modelName, fieldName, c.Collection)) + continue Next + } + // fmt.Printf("Relation %s/%s to %s/%s\n", modelName, fieldName, c.Collection, c.ToField.Name) + toField, ok := toModel.Fields[c.ToField.Name] + if !ok { + errs.append(fmt.Errorf("%s/%s directs to nonexisting collectionfield `%s/%s`", modelName, fieldName, c.Collection, c.ToField.Name)) + continue Next + } + + if !relation[toField.Type] { + errs.append(fmt.Errorf("%s/%s directs to `%s/%s`, but it is not a relation, but %s", modelName, fieldName, c.Collection, c.ToField.Name, toField.Type)) + continue Next + } + + } + } + } + if errs.empty() { + return nil + } + return errs +} + +func validateTemplatePrefixes(models map[string]Model) error { + errs := &ErrorList{ + Name: "template prefixes validator", + intent: 1, + } + for modelName, model := range models { + prefixes := map[string]bool{} + for fieldName := range model.Fields { + i := strings.Index(fieldName, "$") + if i < 0 { + continue + } + prefix := fieldName[0:i] + if prefixes[prefix] { + errs.append(fmt.Errorf("Duplicate template prefix %s in %s", prefix, modelName)) + } + prefixes[prefix] = true + } + } + if errs.empty() { + return nil + } + return errs +} + +// scalarTypes are the main types. All scalarTypes can be used as a list. +// JSON[], timestamp[] etc. +func scalarTypes() map[string]bool { + s := []string{ + "string", + "number", + "boolean", + "JSON", + "HTMLPermissive", + "HTMLStrict", + "float", + "decimal(6)", + "timestamp", + } + out := make(map[string]bool) + for _, t := range s { + out[t] = true + } + return out +} + +// relationTypes are realtion types in realtion to other fields. +func relationTypes() map[string]bool { + s := []string{ + "relation", + "relation-list", + "generic-relation", + "generic-relation-list", + "template", + } + out := make(map[string]bool) + for _, t := range s { + out[t] = true + } + return out +} diff --git a/docs/modelsvalidator/models/check_test.go b/docs/modelsvalidator/models/check_test.go new file mode 100644 index 000000000..72252329b --- /dev/null +++ b/docs/modelsvalidator/models/check_test.go @@ -0,0 +1,125 @@ +package models_test + +import ( + "errors" + "strings" + "testing" + + "github.com/OpenSlides/Openslides/modelsvalidator/models" +) + +const yamlUnknownFieldType = `--- +some_model: + field: unknown +` + +const yamlNonExistingModel = `--- +some_model: + no_other_model: + type: relation + to: not_existing/field + no_other_field: + type: relation + to: other_model/bar +other_model: + foo: string +` + +const yamlNonExistingField = `--- +some_model: + no_other_field: + type: relation + to: other_model/bar +other_model: + foo: string +` + +const yamlDuplicateTemplatePrefix = `--- +some_model: + field_$_1: number + field_$_2: number +` + +const yamlWrongReverseRelaitonType = `--- +some_model: + other_model: + type: relation + to: other_model/field +other_model: + field: HTMLStrict +` + +func TestCheck(t *testing.T) { + for _, tt := range []struct { + name string + yaml string + err string + }{ + { + "unknown type", + yamlUnknownFieldType, + "Unknown type `unknown` in some_model/field", + }, + { + "non-existing model", + yamlNonExistingModel, + "some_model/no_other_model directs to nonexisting model `not_existing`", + }, + { + "non-existing Field", + yamlNonExistingField, + "some_model/no_other_field directs to nonexisting collectionfield `other_model/bar`", + }, + { + "duplicate template prefix", + yamlDuplicateTemplatePrefix, + "Duplicate template prefix field_ in some_model", + }, + { + "wrong reverse relation type", + yamlWrongReverseRelaitonType, + "some_model/other_model directs to `other_model/field`, but it is not a relation, but HTMLStrict", + }, + } { + t.Run(tt.name, func(t *testing.T) { + data, err := models.Unmarshal(strings.NewReader(tt.yaml)) + if err != nil { + t.Fatalf("Can not unmarshal yaml: %v", err) + } + gotErr := models.Check(data) + if tt.err == "" { + if gotErr != nil { + t.Errorf("Models.Check() returned an unexepcted error: %v", err) + } + return + } + + if gotErr == nil { + t.Fatalf("Models.Check() did not return an error, expected: %v", tt.err) + } + + var errList *models.ErrorList + if !errors.As(gotErr, &errList) { + t.Fatalf("Models.Check() did not return a ListError, got: %v", gotErr) + } + + var found bool + for _, err := range errList.Errs { + var errList *models.ErrorList + if !errors.As(err, &errList) { + continue + } + + for _, err := range errList.Errs { + if err.Error() == tt.err { + found = true + } + } + } + + if !found { + t.Errorf("Models.Check() returned %v, expected %v", gotErr, tt.err) + } + }) + } +} diff --git a/docs/modelsvalidator/models/error.go b/docs/modelsvalidator/models/error.go new file mode 100644 index 000000000..c67aca452 --- /dev/null +++ b/docs/modelsvalidator/models/error.go @@ -0,0 +1,38 @@ +package models + +import ( + "fmt" + "strings" +) + +// ErrorList is an error that contains a list of other errors. +type ErrorList struct { + Name string + intent int + Errs []error +} + +func (e *ErrorList) append(err error) { + if err == nil { + return + } + + e.Errs = append(e.Errs, err) +} + +func (e ErrorList) Error() string { + intent := strings.Repeat(" ", e.intent) + var msgs []string + for _, err := range e.Errs { + msgs = append(msgs, fmt.Sprintf("%s* %v", intent, err)) + } + msg := strings.Join(msgs, "\n") + if e.Name != "" { + return fmt.Sprintf("%s:\n%s", e.Name, msg) + } + return msg +} + +func (e *ErrorList) empty() bool { + return len(e.Errs) == 0 +} diff --git a/docs/modelsvalidator/models/models.go b/docs/modelsvalidator/models/models.go new file mode 100644 index 000000000..78f45c308 --- /dev/null +++ b/docs/modelsvalidator/models/models.go @@ -0,0 +1,233 @@ +package models + +import ( + "fmt" + "io" + "strings" + + "gopkg.in/yaml.v3" +) + +// Unmarshal parses the content of models.yml to a datastruct.q +func Unmarshal(r io.Reader) (map[string]Model, error) { + var m map[string]Model + if err := yaml.NewDecoder(r).Decode(&m); err != nil { + return nil, fmt.Errorf("decoding models: %w", err) + } + return m, nil +} + +// Model replresents one model from models.yml. +type Model struct { + Fields map[string]Field +} + +// UnmarshalYAML decodes a yaml model to models.Model. +func (m *Model) UnmarshalYAML(node *yaml.Node) error { + return node.Decode(&m.Fields) +} + +// Field of a model. +type Field struct { + Type string + relation Relation + template *AttributeTemplate +} + +// Relation returns the relation object if the Field is a relation or a +// template with a relation. In other cases, it returns nil. +func (a *Field) Relation() Relation { + if a.relation != nil { + return a.relation + } + + if a.template != nil && a.template.Fields.relation != nil { + return a.template.Fields.relation + } + return nil +} + +// UnmarshalYAML decodes a model attribute from yaml. +func (a *Field) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err == nil { + a.Type = s + return nil + } + + var typer struct { + Type string `yaml:"type"` + } + if err := value.Decode(&typer); err != nil { + return fmt.Errorf("field object without type: %w", err) + } + + a.Type = typer.Type + switch typer.Type { + case "relation": + fallthrough + case "relation-list": + var relation AttributeRelation + if err := value.Decode(&relation); err != nil { + return fmt.Errorf("invalid object of type %s at line %d object: %w", typer.Type, value.Line, err) + } + a.relation = &relation + case "generic-relation": + fallthrough + case "generic-relation-list": + var relation AttributeGenericRelation + if err := value.Decode(&relation); err != nil { + return fmt.Errorf("invalid object of type %s at line %d object: %w", typer.Type, value.Line, err) + } + a.relation = &relation + case "template": + var template AttributeTemplate + if err := value.Decode(&template); err != nil { + return fmt.Errorf("invalid object of type template object in line %d: %w", value.Line, err) + } + a.template = &template + } + return nil +} + +// Relation represents some kind of relation between fields. +type Relation interface { + ToCollections() []ToCollectionField +} + +// ToCollectionField represents a field and a collection +type ToCollectionField struct { + Collection string `yaml:"collection"` + ToField ToField `yaml:"field"` +} + +// UnmarshalYAML decodes the models.yml to a To object. +func (t *ToCollectionField) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err == nil { + cf := strings.Split(s, "/") + if len(cf) != 2 { + return fmt.Errorf("invalid value of `to` in line %d, expected one `/`: %s", value.Line, s) + } + t.Collection = cf[0] + t.ToField.Name = cf[1] + return nil + } + + var d struct { + Collection string `yaml:"collection"` + Field ToField `yaml:"field"` + } + if err := value.Decode(&d); err != nil { + return fmt.Errorf("decoding to collection field at line %d: %w", value.Line, err) + } + t.Collection = d.Collection + t.ToField = d.Field + return nil +} + +type ToField struct { + Name string `yaml:"name"` + Type string `yaml:"type"` +} + +// UnmarshalYAML decodes the models.yml to a ToField object. +func (t *ToField) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err == nil { + t.Name = s + t.Type = "normal" + return nil + } + + var d struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + } + if err := value.Decode(&d); err != nil { + return fmt.Errorf("decoding to field at line %d: %w", value.Line, err) + } + t.Name = d.Name + t.Type = d.Type + return nil +} + +// AttributeRelation is a relation or relation-list field. +type AttributeRelation struct { + To To `yaml:"to"` +} + +// ToCollection returns the names of the collections there the attribute points +// to. It is allways a slice with one element. +func (r AttributeRelation) ToCollections() []ToCollectionField { + return []ToCollectionField{r.To.CollectionField} +} + +// To is shows a Relation where to point to. +type To struct { + CollectionField ToCollectionField +} + +// UnmarshalYAML decodes the models.yml to a To object. +func (t *To) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err == nil { + cf := strings.Split(s, "/") + if len(cf) != 2 { + return fmt.Errorf("invalid value of `to` in line %d, expected one `/`: %s", value.Line, s) + } + t.CollectionField.Collection = cf[0] + t.CollectionField.ToField.Name = cf[1] + return nil + } + + if err := value.Decode(&(t.CollectionField)); err != nil { + return fmt.Errorf("decoding to field at line %d: %w", value.Line, err) + } + return nil +} + +// AttributeGenericRelation is a generic-relation or generic-relation-list field. +type AttributeGenericRelation struct { + To ToGeneric `yaml:"to"` +} + +// ToCollections returns all collection, where the generic field could point to. +func (r AttributeGenericRelation) ToCollections() []ToCollectionField { + return r.To.CollectionFields +} + +// AttributeTemplate represents a template field. +type AttributeTemplate struct { + Replacement string `yaml:"replacement"` + Fields Field `yaml:"fields"` +} + +// ToGeneric is like a To object, but for generic relations. +type ToGeneric struct { + CollectionFields []ToCollectionField +} + +func (t *ToGeneric) UnmarshalYAML(value *yaml.Node) error { + var d struct { + Collections []string `yaml:"collections"` + Field ToField `yaml:"field"` + } + if err := value.Decode(&d); err == nil { + t.CollectionFields = make([]ToCollectionField, len(d.Collections)) + for i, collection := range d.Collections { + t.CollectionFields[i].Collection = collection + t.CollectionFields[i].ToField = d.Field + } + return nil + } + + var e struct { + CollectionFields []ToCollectionField `yaml:"collections"` + } + if err := value.Decode(&e); err != nil { + return fmt.Errorf("decoding to generic field at line %d: %w", value.Line, err) + } + t.CollectionFields = e.CollectionFields + return nil +} diff --git a/openslides-client b/openslides-client index ff4d23361..6ab4acfcd 160000 --- a/openslides-client +++ b/openslides-client @@ -1 +1 @@ -Subproject commit ff4d2336192c6f7696244008f1496f6fe7028500 +Subproject commit 6ab4acfcdb70bba76ca42c21f78f5462148d073c