Restructure polls
This commit is contained in:
parent
5019403a8e
commit
99b0bc6e3b
13
.github/workflows/models.yml
vendored
13
.github/workflows/models.yml
vendored
@ -10,13 +10,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: 1.15
|
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
|
- name: Check out code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build validator
|
||||||
|
run: go build ./cmd/modelsvalidator
|
||||||
|
working-directory: docs/modelsvalidator
|
||||||
|
env:
|
||||||
|
GO111MODULE: on
|
||||||
|
|
||||||
- name: Validate models.yml
|
- name: Validate models.yml
|
||||||
run: $HOME/go/bin/modelsvalidate docs/models.yml
|
run: docs/modelsvalidator/modelsvalidator docs/models.yml
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -7,6 +7,10 @@
|
|||||||
.idea
|
.idea
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|
||||||
|
# docs
|
||||||
|
docs/modelsvalidator/modelsvalidator
|
||||||
|
dev-commands/export.json
|
||||||
|
|
||||||
# certs
|
# certs
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
431
docs/models.yml
431
docs/models.yml
@ -23,8 +23,19 @@
|
|||||||
# `motion/category_id`. The type indicates that there are many
|
# `motion/category_id`. The type indicates that there are many
|
||||||
# motion ids.
|
# motion ids.
|
||||||
# - Generic relations: The difference to non-generic relations is that you have a
|
# - 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
|
# list of possible collections. Therefore we split the simple notation up to the
|
||||||
# properties `collection` and `field`.
|
# 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
|
# - on_delete: This fields determines what should happen with the foreign model if
|
||||||
# this model gets deleted. Possible values are:
|
# this model gets deleted. Possible values are:
|
||||||
# - SET_NULL (default): delete the id from the foreign key
|
# - SET_NULL (default): delete the id from the foreign key
|
||||||
@ -173,54 +184,36 @@ user:
|
|||||||
fields:
|
fields:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_submitter/user_id
|
to: motion_submitter/user_id
|
||||||
motion_poll_voted_$_ids:
|
poll_voted_$_ids:
|
||||||
type: template
|
type: template
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
fields:
|
fields:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_poll/voted_ids
|
to: poll/voted_ids
|
||||||
motion_vote_$_ids:
|
option_$_ids:
|
||||||
type: template
|
type: template
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
fields:
|
fields:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_vote/user_id
|
to: option/content_object_id
|
||||||
motion_delegated_vote_$_ids:
|
vote_$_ids:
|
||||||
type: template
|
type: template
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
fields:
|
fields:
|
||||||
type: relation-list
|
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:
|
assignment_candidate_$_ids:
|
||||||
type: template
|
type: template
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
fields:
|
fields:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: assignment_candidate/user_id
|
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:
|
vote_delegated_$_to_id:
|
||||||
type: template
|
type: template
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
@ -487,6 +480,7 @@ meeting:
|
|||||||
motions_export_submitter_recommendation: boolean
|
motions_export_submitter_recommendation: boolean
|
||||||
motions_export_follow_recommendation: boolean
|
motions_export_follow_recommendation: boolean
|
||||||
|
|
||||||
|
# Motion poll
|
||||||
motion_poll_ballot_paper_selection:
|
motion_poll_ballot_paper_selection:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@ -532,6 +526,7 @@ meeting:
|
|||||||
assignemnts_export_title: string
|
assignemnts_export_title: string
|
||||||
assignments_export_preamble: string
|
assignments_export_preamble: string
|
||||||
|
|
||||||
|
# Assignment polls
|
||||||
assignment_poll_ballot_paper_selection:
|
assignment_poll_ballot_paper_selection:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@ -549,10 +544,32 @@ meeting:
|
|||||||
type: relation-list
|
type: relation-list
|
||||||
to: group/used_as_assignment_poll_default_id
|
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:
|
projector_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: projector/meeting_id
|
to: projector/meeting_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
|
projection_ids:
|
||||||
|
type: relation-list
|
||||||
|
to: projection/meeting_id
|
||||||
|
on_delete: CASCADE
|
||||||
projectiondefault_ids:
|
projectiondefault_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: projectiondefault/meeting_id
|
to: projectiondefault/meeting_id
|
||||||
@ -577,6 +594,10 @@ meeting:
|
|||||||
type: relation-list
|
type: relation-list
|
||||||
to: list_of_speakers/meeting_id
|
to: list_of_speakers/meeting_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
|
speaker_ids:
|
||||||
|
type: relation-list
|
||||||
|
to: speaker/meeting_id
|
||||||
|
on_delete: CASCADE
|
||||||
topic_ids:
|
topic_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: topic/meeting_id
|
to: topic/meeting_id
|
||||||
@ -613,38 +634,6 @@ meeting:
|
|||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_statute_paragraph/meeting_id
|
to: motion_statute_paragraph/meeting_id
|
||||||
on_delete: CASCADE
|
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:
|
motion_comment_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_comment/meeting_id
|
to: motion_comment/meeting_id
|
||||||
@ -661,17 +650,29 @@ meeting:
|
|||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_state/meeting_id
|
to: motion_state/meeting_id
|
||||||
on_delete: CASCADE
|
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:
|
assignment_candidate_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: assignment_candidate/meeting_id
|
to: assignment_candidate/meeting_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
assignment_option_ids:
|
personal_note_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: assignment_option/meeting_id
|
to: personal_note/meeting_id
|
||||||
on_delete: CASCADE
|
|
||||||
assignment_vote_ids:
|
|
||||||
type: relation-list
|
|
||||||
to: assignment_vote/meeting_id
|
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
|
|
||||||
# Logos and Fonts
|
# Logos and Fonts
|
||||||
@ -778,13 +779,9 @@ group:
|
|||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_comment_section/write_group_ids
|
to: motion_comment_section/write_group_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
motion_poll_ids:
|
poll_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_poll/entitled_group_ids
|
to: poll/entitled_group_ids
|
||||||
equal_fields: meeting_id
|
|
||||||
assignment_poll_ids:
|
|
||||||
type: relation-list
|
|
||||||
to: assignment_poll/entitled_group_ids
|
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
used_as_motion_poll_default_id:
|
used_as_motion_poll_default_id:
|
||||||
type: relation
|
type: relation
|
||||||
@ -792,6 +789,9 @@ group:
|
|||||||
used_as_assignment_poll_default_id:
|
used_as_assignment_poll_default_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: meeting/assignment_poll_default_group_ids
|
to: meeting/assignment_poll_default_group_ids
|
||||||
|
used_as_poll_default_id:
|
||||||
|
type: relation
|
||||||
|
to: meeting/poll_default_group_ids
|
||||||
meeting_id:
|
meeting_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: meeting/group_ids
|
to: meeting/group_ids
|
||||||
@ -813,7 +813,7 @@ personal_note:
|
|||||||
content_object_id:
|
content_object_id:
|
||||||
type: generic-relation
|
type: generic-relation
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
field: personal_note_ids
|
field: personal_note_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
@ -831,7 +831,7 @@ tag:
|
|||||||
tagged_ids:
|
tagged_ids:
|
||||||
type: generic-relation-list
|
type: generic-relation-list
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- agenda_item
|
- agenda_item
|
||||||
- assignment
|
- assignment
|
||||||
- motion
|
- motion
|
||||||
@ -878,7 +878,7 @@ agenda_item:
|
|||||||
content_object_id:
|
content_object_id:
|
||||||
type: generic-relation
|
type: generic-relation
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
- motion_block
|
- motion_block
|
||||||
- assignment
|
- assignment
|
||||||
@ -918,7 +918,7 @@ list_of_speakers:
|
|||||||
content_object_id:
|
content_object_id:
|
||||||
type: generic-relation
|
type: generic-relation
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
- motion_block
|
- motion_block
|
||||||
- assignment
|
- assignment
|
||||||
@ -1000,6 +1000,11 @@ topic:
|
|||||||
required: true
|
required: true
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
|
option_ids:
|
||||||
|
type: relation-list
|
||||||
|
to: option/content_object_id
|
||||||
|
on_delete: CASCADE
|
||||||
|
equal_fields: meeting_id
|
||||||
tag_ids:
|
tag_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: tag/tagged_ids
|
to: tag/tagged_ids
|
||||||
@ -1084,7 +1089,7 @@ motion:
|
|||||||
recommendation_extension_reference_ids:
|
recommendation_extension_reference_ids:
|
||||||
type: generic-relation-list
|
type: generic-relation-list
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
field: referenced_in_motion_recommendation_extension_ids
|
field: referenced_in_motion_recommendation_extension_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
@ -1116,7 +1121,12 @@ motion:
|
|||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
poll_ids:
|
poll_ids:
|
||||||
type: relation-list
|
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
|
on_delete: CASCADE
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
change_recommendation_ids:
|
change_recommendation_ids:
|
||||||
@ -1463,26 +1473,91 @@ motion_statute_paragraph:
|
|||||||
to: meeting/motion_statute_paragraph_ids
|
to: meeting/motion_statute_paragraph_ids
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
motion_poll:
|
poll:
|
||||||
id: number
|
id: number
|
||||||
pollmethod: string
|
description: string
|
||||||
state: number
|
title:
|
||||||
type: string
|
type: string
|
||||||
title: string
|
required: true
|
||||||
onehundred_percent_base: string
|
type:
|
||||||
majority_method: string
|
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)
|
votesvalid: decimal(6)
|
||||||
votesinvalid: decimal(6)
|
votesinvalid: decimal(6)
|
||||||
votescast: decimal(6)
|
votescast: decimal(6)
|
||||||
user_has_voted: boolean # This is user specific and set during restriction
|
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:
|
content_object_id: # Note: must not be set - it is allowed to have standalone polls
|
||||||
type: relation
|
type: generic-relation
|
||||||
to: motion/poll_ids
|
to:
|
||||||
|
collections:
|
||||||
|
- motion
|
||||||
|
- assignment
|
||||||
|
field: poll_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
option_ids:
|
option_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_option/poll_id
|
to: option/poll_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
voted_ids:
|
voted_ids:
|
||||||
@ -1490,12 +1565,12 @@ motion_poll:
|
|||||||
to:
|
to:
|
||||||
collection: user
|
collection: user
|
||||||
field:
|
field:
|
||||||
name: motion_poll_voted_$_ids
|
name: poll_voted_$_ids
|
||||||
type: structured-relation
|
type: structured-relation
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
entitled_group_ids:
|
entitled_group_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: group/motion_poll_ids
|
to: group/poll_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
projection_ids:
|
projection_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
@ -1507,43 +1582,63 @@ motion_poll:
|
|||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
meeting_id:
|
meeting_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: meeting/motion_poll_ids
|
to: meeting/poll_ids
|
||||||
|
|
||||||
motion_option:
|
option:
|
||||||
id: number
|
id: number
|
||||||
|
weight:
|
||||||
|
type: number
|
||||||
|
default: 10000
|
||||||
|
text: HTMLStrict
|
||||||
yes: decimal(6)
|
yes: decimal(6)
|
||||||
no: decimal(6)
|
no: decimal(6)
|
||||||
abstain: decimal(6)
|
abstain: decimal(6)
|
||||||
|
|
||||||
poll_id:
|
poll_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: motion_poll/option_ids
|
to: poll/option_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
|
required: true
|
||||||
vote_ids:
|
vote_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: motion_vote/option_id
|
to: vote/option_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
equal_fields: meeting_id
|
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:
|
meeting_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: meeting/motion_option_ids
|
to: meeting/option_ids
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
motion_vote:
|
vote:
|
||||||
id: number
|
id: number
|
||||||
weight: decimal(6)
|
weight: decimal(6)
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
option_id:
|
option_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: motion_option/vote_ids
|
to: option/vote_ids
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
|
required: true
|
||||||
user_id:
|
user_id:
|
||||||
type: relation
|
type: relation
|
||||||
to:
|
to:
|
||||||
collection: user
|
collection: user
|
||||||
field:
|
field:
|
||||||
name: motion_vote_$_ids
|
name: vote_$_ids
|
||||||
type: structured-relation
|
type: structured-relation
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
delegated_user_id:
|
delegated_user_id:
|
||||||
@ -1551,12 +1646,12 @@ motion_vote:
|
|||||||
to:
|
to:
|
||||||
collection: user
|
collection: user
|
||||||
field:
|
field:
|
||||||
name: motion_delegated_vote_$_ids
|
name: vote_delegated_vote_$_ids
|
||||||
type: structured-relation
|
type: structured-relation
|
||||||
replacement: meeting_id
|
replacement: meeting_id
|
||||||
meeting_id:
|
meeting_id:
|
||||||
type: relation
|
type: relation
|
||||||
to: meeting/motion_vote_ids
|
to: meeting/vote_ids
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
assignment:
|
assignment:
|
||||||
@ -1586,7 +1681,7 @@ assignment:
|
|||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
poll_ids:
|
poll_ids:
|
||||||
type: relation-list
|
type: relation-list
|
||||||
to: assignment_poll/assignment_id
|
to: poll/content_object_id
|
||||||
on_delete: CASCADE
|
on_delete: CASCADE
|
||||||
equal_fields: meeting_id
|
equal_fields: meeting_id
|
||||||
agenda_item_id:
|
agenda_item_id:
|
||||||
@ -1644,120 +1739,6 @@ assignment_candidate:
|
|||||||
to: meeting/assignment_candidate_ids
|
to: meeting/assignment_candidate_ids
|
||||||
required: true
|
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
|
# Mediafiles are delivered by the mediafile server with the URL
|
||||||
# `<media-prefix>/media/<meeting_id>/path`
|
# `<media-prefix>/media/<meeting_id>/path`
|
||||||
mediafile:
|
mediafile:
|
||||||
@ -1816,7 +1797,7 @@ mediafile:
|
|||||||
attachment_ids:
|
attachment_ids:
|
||||||
type: generic-relation-list
|
type: generic-relation-list
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
- topic
|
- topic
|
||||||
- assignment
|
- assignment
|
||||||
@ -1880,7 +1861,7 @@ projector:
|
|||||||
current_element_ids:
|
current_element_ids:
|
||||||
type: generic-relation-list
|
type: generic-relation-list
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
- mediafile
|
- mediafile
|
||||||
- list_of_speakers
|
- list_of_speakers
|
||||||
@ -1889,8 +1870,7 @@ projector:
|
|||||||
- agenda_item
|
- agenda_item
|
||||||
- topic
|
- topic
|
||||||
- user
|
- user
|
||||||
- assignment_poll
|
- poll
|
||||||
- motion_poll
|
|
||||||
- projector_message
|
- projector_message
|
||||||
- projector_countdown
|
- projector_countdown
|
||||||
field: current_projector_ids
|
field: current_projector_ids
|
||||||
@ -1939,7 +1919,7 @@ projection:
|
|||||||
element_id:
|
element_id:
|
||||||
type: generic-relation
|
type: generic-relation
|
||||||
to:
|
to:
|
||||||
collection:
|
collections:
|
||||||
- motion
|
- motion
|
||||||
- mediafile
|
- mediafile
|
||||||
- list_of_speakers
|
- list_of_speakers
|
||||||
@ -1948,8 +1928,7 @@ projection:
|
|||||||
- agenda_item
|
- agenda_item
|
||||||
- topic
|
- topic
|
||||||
- user
|
- user
|
||||||
- assignment_poll
|
- poll
|
||||||
- motion_poll
|
|
||||||
- projector_message
|
- projector_message
|
||||||
- projector_countdown
|
- projector_countdown
|
||||||
field: projection_ids
|
field: projection_ids
|
||||||
|
20
docs/modelsvalidator/README.md
Normal file
20
docs/modelsvalidator/README.md
Normal file
@ -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.
|
55
docs/modelsvalidator/cmd/modelsvalidator/main.go
Normal file
55
docs/modelsvalidator/cmd/modelsvalidator/main.go
Normal file
@ -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
|
||||||
|
}
|
7
docs/modelsvalidator/go.mod
Normal file
7
docs/modelsvalidator/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/OpenSlides/Openslides/modelsvalidator
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||||
|
)
|
3
docs/modelsvalidator/go.sum
Normal file
3
docs/modelsvalidator/go.sum
Normal file
@ -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=
|
156
docs/modelsvalidator/models/check.go
Normal file
156
docs/modelsvalidator/models/check.go
Normal file
@ -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
|
||||||
|
}
|
125
docs/modelsvalidator/models/check_test.go
Normal file
125
docs/modelsvalidator/models/check_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
38
docs/modelsvalidator/models/error.go
Normal file
38
docs/modelsvalidator/models/error.go
Normal file
@ -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
|
||||||
|
}
|
233
docs/modelsvalidator/models/models.go
Normal file
233
docs/modelsvalidator/models/models.go
Normal file
@ -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
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit ff4d2336192c6f7696244008f1496f6fe7028500
|
Subproject commit 6ab4acfcdb70bba76ca42c21f78f5462148d073c
|
Loading…
Reference in New Issue
Block a user