From 2c4dd561f490b4ee65ac8402f863dffff8f69aa8 Mon Sep 17 00:00:00 2001 From: jstet Date: Sun, 27 Sep 2026 18:38:50 +0200 Subject: [PATCH] feat(ddi)!: every form field in the codebook, standard DDI first (#153) convention:ddiFields maps every Instrument field to a standard DDI 2.5 element or attribute, a typed `cdl:` note where DDI has none, or a documented loss; a unit test fails when a field has none of these. Standard DDI: - the hint is `qstn/postQTxt` (was merged into `preQTxt`); `preQTxt` is only a note row before the question or a grid's / select_multiple's shared text. Grid members and select_multiple options now keep their hints, so `hint-dropped` is LimeSurvey-only. - `varFormat/@category` date / time; `var/@dcml="0"` for integer; `valrng/range` for a range's start/end; - `qstn/@seqNo` (position among the data questions) and `qstn/backward` (the var / varGrp IDs its relevant refers to). Typed notes: `cdl:default`, `cdl:appearance`, `cdl:parameters` (what no element holds), a group's `cdl:hint` per language, `cdl:exclusive` on a select_multiple's varGrp, `stdyDscr/notes type="cdl:setting"` for style. xlsform2lstsv writes `num_value_int_only=1` for an integer, and a LimeSurvey N with it reads back as integer, so integer survives both ways. BREAKING CHANGE: the question hint moves from `preQTxt` to `postQTxt`; readers taking the hint from `preQTxt` must read `postQTxt`. Co-Authored-By: Claude Opus 5.5 (1M context) --- ARCHITECTURE.md | 2 +- README.md | 9 +- registry/conventions/ddiFields.jsonld | 194 +++++++++++++++ registry/conventions/languageTagging.jsonld | 2 + registry/entities/date/ddi.xml | 4 +- registry/entities/decimal/ddi.xml | 2 +- registry/entities/grid/ddi.xml | 4 +- registry/entities/integer/ddi.xml | 4 +- registry/entities/integer/definition.jsonld | 8 +- registry/entities/integer/tsv.tsv | 14 +- registry/entities/range/ddi.xml | 6 +- registry/entities/select_multiple/ddi.xml | 4 +- .../select_multiple_long_list/ddi.xml | 2 +- .../entities/select_multiple_other/ddi.xml | 9 +- registry/entities/select_one/ddi.xml | 2 +- .../entities/select_one_long_list/ddi.xml | 2 +- registry/entities/select_one_other/ddi.xml | 5 +- registry/entities/text/ddi.xml | 2 +- registry/entities/time/ddi.xml | 4 +- src/ddi/codebook.ts | 194 +++++++++++---- src/ddi/fields.ts | 109 ++++++++ src/ddi/fromInstrument.ts | 18 +- src/ddi/logic.ts | 25 +- src/ddi/types.ts | 14 +- src/generated/TypeMappings.ts | 2 + src/generated/conventions.json | 162 ++++++++++++ src/generated/conventions.ts | 162 ++++++++++++ src/instrument/lstsvTypes.ts | 15 +- src/pipelines/README.md | 18 +- src/pipelines/lstsv2xlsform/README.md | 10 +- src/pipelines/xlsform2lstsv/parameters.ts | 34 ++- src/utils/parameters.ts | 13 + src/xlsform/validate.ts | 36 +-- .../fixtures/surveys/all_types_survey/ddi.xml | 90 ++++--- .../fixtures/surveys/all_types_survey/tsv.tsv | 122 ++++----- .../surveys/appearances_survey/ddi.xml | 25 +- tests/fixtures/surveys/basic_survey/ddi.xml | 8 +- tests/fixtures/surveys/basic_survey/tsv.tsv | 24 +- .../fixtures/surveys/bilingual_survey/ddi.xml | 27 +- tests/fixtures/surveys/complex_survey/ddi.xml | 30 ++- tests/fixtures/surveys/complex_survey/tsv.tsv | 48 ++-- .../surveys/complex_xpath_survey/ddi.xml | 23 +- .../surveys/complex_xpath_survey/tsv.tsv | 28 +-- tests/fixtures/surveys/grid_survey/ddi.xml | 20 +- tests/fixtures/surveys/grid_survey/tsv.tsv | 42 ++-- tests/fixtures/surveys/hints_survey/ddi.xml | 21 +- tests/fixtures/surveys/hints_survey/tsv.tsv | 30 +-- .../surveys/multilingual_survey/ddi.xml | 14 +- .../surveys/multilingual_survey/tsv.tsv | 62 ++--- .../fixtures/surveys/multipage_survey/ddi.xml | 11 +- .../fixtures/surveys/multipage_survey/tsv.tsv | 28 +-- .../fixtures/surveys/settings_survey/ddi.xml | 7 +- tests/fixtures/surveys/testA/ddi.xml | 71 +++--- tests/fixtures/surveys/testA/tsv.tsv | 2 +- tests/fixtures/surveys/testB/ddi.xml | 204 ++++++++++----- .../validation_relevance_survey/ddi.xml | 33 ++- .../validation_relevance_survey/tsv.tsv | 40 +-- tests/ts/contract/surveySnapshots.test.ts | 6 +- tests/ts/unit/ddi/fields.test.ts | 232 ++++++++++++++++++ tests/ts/unit/ddi/multilingual.test.ts | 4 +- .../pipelines/xlsform2ddi/codebook.test.ts | 18 +- .../pipelines/xlsform2ddi/explicit.test.ts | 4 +- .../unit/pipelines/xlsform2ddi/hints.test.ts | 26 +- .../test_registry_schematron_conformance.py | 6 +- 64 files changed, 1779 insertions(+), 618 deletions(-) create mode 100644 registry/conventions/ddiFields.jsonld create mode 100644 src/ddi/fields.ts create mode 100644 src/utils/parameters.ts create mode 100644 tests/ts/unit/ddi/fields.test.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index afb9901..1642850 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -157,7 +157,7 @@ The canonical `Variable` (`src/ddi/types.ts`) is what survives an emit: - **`relevant`, `constraint`, `constraint_message`, `required`** survive (#151). DDI Codebook 2.5 has no expression syntax, so each goes in twice: readable (`` prose, `` for a simple numeric range) and exact, in a typed ``. `convention:logicMapping` (`ddiEncoding`) defines the notes. A group's own condition is on its `varGrp`; a variable's `` states its groups' conditions too. - **Groups and order** survive (#152): every group is a `` (a plain one `type="section"`), nested through `@varGrp`, and the ``s and data columns follow the survey. -- **no `default`, per-question `appearance`, `calculation`, group hint** yet (#153). A question's `hint` and `guidance_hint` do survive, as `` and ``. +- **Every other form field** (#153, `convention:ddiFields`): standard DDI where it has a home (the hint as `postQTxt`, `guidance_hint` as `ivuInstr`, `varFormat/@category` for date and time, `var/@dcml="0"` for integer, `valrng` for a range, `qstn/@seqNo` and `qstn/backward`), else a typed note (`cdl:default`, `cdl:appearance`, `cdl:parameters`, a group's `cdl:hint`, `cdl:exclusive`, `cdl:setting`). A unit test fails when a model field has neither nor a documented loss. Not carried: `calculation` and other unlifted columns, a choice list's name, the `or_other` shorthand as such. Compare `lstsv2xlsform`, which *is* implemented: a LimeSurvey structure TSV carries `relevance`, `em_validation_q`, `mandatory`, `default` and the `!`/`T` type overrides. It is a form definition in a different dialect, so reversing it is a translation problem. Reversing a codebook without those pieces is a *reconstruction* problem, and they cannot be inferred from it. diff --git a/README.md b/README.md index e07b1a7..da26649 100644 --- a/README.md +++ b/README.md @@ -188,15 +188,16 @@ records how each maps onto it. `lstsv2ddi` and `lstsv2xlsform` (the reverse paths). All are lossy for some types: nested groups flatten in LimeSurvey, choice codes over 5 chars truncate, `select_multiple` becomes N binary variables, and the reverse paths cannot -recover a select's authored `list_name` or tell `integer` from `decimal`. +recover a select's authored `list_name`. **DDI has no way back yet:** there is no `ddi2xlsform` or `ddi2lstsv`. A CDL codebook carries skip logic, validation and `required`: each condition as a readable `` sentence (and a simple numeric range as ``), plus the exact expression in a typed note such as `` (`convention:logicMapping`). Defaults, appearances -and the group structure are not in it yet, so a reversed form would still look -right and behave wrongly. #155 tracks the rest. +subject="xlsform-xpath">` (`convention:logicMapping`). Groups, order, hints, +defaults, appearances and parameters are in it too, in standard DDI where it +has a place and typed notes where not (`convention:ddiFields`). The reverse +parser that reads them back is #154. ## Errors and warnings diff --git a/registry/conventions/ddiFields.jsonld b/registry/conventions/ddiFields.jsonld new file mode 100644 index 0000000..7177b77 --- /dev/null +++ b/registry/conventions/ddiFields.jsonld @@ -0,0 +1,194 @@ +{ + "@context": { + "@version": 1.1, + "@vocab": "https://civic-data.de/survey/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "type": "https://civic-data.de/survey/type/", + "variant": "https://civic-data.de/survey/variant/", + "composite": "https://civic-data.de/survey/composite/", + "appearance": "https://civic-data.de/survey/appearance/", + "convention": "https://civic-data.de/survey/convention/", + "vocab": "https://civic-data.de/survey/vocab/", + "meta": "https://civic-data.de/survey/meta/", + "classes": { + "@id": "meta:classes", + "@type": "@json" + }, + "relations": { + "@id": "meta:relations", + "@type": "@json" + }, + "rule": { + "@id": "https://civic-data.de/survey/rule", + "@type": "@json" + } + }, + "@graph": [ + { + "@id": "convention:ddiFields", + "@type": "GlobalConvention", + "skos:prefLabel": "Form fields in DDI Codebook", + "rule": { + "description": "Where each field of a form (the Instrument model, src/instrument/types.ts) goes in a CDL DDI codebook (formtransform#153): a standard DDI 2.5 element or attribute wherever one fits, a typed note `\">` (the XSD's controlled-vocabulary extension point, as in convention:logicMapping) only where DDI 2.5 has none, or a documented loss. A unit test fails when a model field has no entry.", + "questionText": { + "preQTxt": "a note row before the question, or a grid's or select_multiple's shared question text", + "qstnLit": "the label", + "postQTxt": "the hint shown to respondents with the question", + "ivuInstr": "guidance_hint (for interviewers and the codebook, not shown)", + "order": "preQTxt, qstnLit, postQTxt, backward, ivuInstr (XSD order)" + }, + "backward": "qstn/backward/@qstn lists the IDs of the questions its own relevant refers to (a var, or the varGrp of a select_multiple or other pair).", + "seqNo": "qstn/@seqNo is the question's position among the form's data questions, from 1; a select_multiple's binaries share their question's.", + "notes": { + "default": { + "type": "cdl:default", + "on": [ + "var", + "varGrp" + ], + "text": "the default cell as authored" + }, + "appearance": { + "type": "cdl:appearance", + "on": [ + "var", + "varGrp" + ], + "text": "the appearance cell, lowercased" + }, + "parameters": { + "type": "cdl:parameters", + "on": [ + "var", + "varGrp" + ], + "text": "the parameters not in a standard element, space-separated key=value" + }, + "hint": { + "type": "cdl:hint", + "on": [ + "varGrp[@type='section' or @type='grid']" + ], + "localized": true + }, + "exclusive": { + "type": "cdl:exclusive", + "on": [ + "varGrp[@type='multipleResp']" + ], + "text": "the exclusive choices' codes, space-separated" + }, + "setting": { + "type": "cdl:setting", + "on": [ + "stdyDscr" + ], + "subject": "the settings key", + "keys": [ + "style" + ] + } + }, + "fields": { + "ItemBase.name": { + "ddi": "var/@name; a group's path in varGrp/@name" + }, + "ItemBase.label": { + "ddi": "qstn/qstnLit; a group's varGrp/txt" + }, + "ItemBase.hint": { + "ddi": "qstn/postQTxt; a group's in a cdl:hint note (varGrp has no hint element)", + "note": "CDL uses postQTxt for the hint shown to respondents with the question, though the XSD describes it as text about what follows the question: DDI 2.5 has no hint element." + }, + "ItemBase.relevant": { + "ddi": "universe + cdl:relevant note (convention:logicMapping)" + }, + "ItemBase.appearance": { + "cdlNote": "cdl:appearance", + "note": "On a var (a select_multiple's or pair's on its varGrp), and on a section; a grid's table-list is its type='grid', so only a grid appearance other than that is noted." + }, + "ItemBase.row": { + "loss": "Columns the model does not lift (media, calculation, …) are not carried." + }, + "GroupItem.kind": { + "ddi": "a group is a varGrp" + }, + "GroupItem.children": { + "ddi": "varGrp/@var and @varGrp (nesting), var element order and qstn/@seqNo (order)" + }, + "GroupItem.closed": { + "loss": "Every group read back is closed." + }, + "QuestionItem.kind": { + "ddi": "a question is a var (a select_multiple: a varGrp of binary vars)" + }, + "QuestionItem.type": { + "ddi": "qstn/@responseDomainType, var/@intrvl, varFormat/@type, varFormat/@category (date, time), var/@dcml='0' (integer)" + }, + "QuestionItem.rawType": { + "loss": "Rebuilt from type, list and the other pattern; the or_other shorthand comes back as the explicit pair, which behaves the same." + }, + "QuestionItem.list": { + "loss": "The list name: questions with identical category sets come back sharing one list." + }, + "QuestionItem.file": { + "ddi": "concept/@vocab (the file is .csv)" + }, + "QuestionItem.orOther": { + "ddi": "varGrp[@type='other'] (convention:other)", + "note": "Comes back as the explicit pair, see rawType." + }, + "QuestionItem.guidanceHint": { + "ddi": "qstn/ivuInstr" + }, + "QuestionItem.constraint": { + "ddi": "cdl:constraint note, plus valrng/range for a simple numeric range (convention:logicMapping)" + }, + "QuestionItem.constraintMessage": { + "cdlNote": "cdl:constraint_message", + "note": "One per language (convention:logicMapping)." + }, + "QuestionItem.required": { + "cdlNote": "cdl:required" + }, + "QuestionItem.default": { + "cdlNote": "cdl:default" + }, + "QuestionItem.parameters": { + "ddi": "range start/end: valrng/range @min/@max; guidance_hint: qstn/ivuInstr; the other parameters (step, randomize, …) in a cdl:parameters note" + }, + "QuestionItem.otherLabel": { + "ddi": "the other answer's catgry/labl and the companion's qstnLit" + }, + "InstrumentChoice.name": { + "ddi": "catgry/catValu" + }, + "InstrumentChoice.label": { + "ddi": "catgry/labl, one per language" + }, + "InstrumentChoice.row": { + "ddi": "exclusive: a cdl:exclusive note on the select_multiple's varGrp listing the codes", + "note": "The choice sheet's other columns are not carried." + }, + "Instrument.languages": { + "ddi": "xml:lang siblings of every text (convention:languageTagging)" + }, + "Instrument.defaultLanguage": { + "ddi": "codeBook/@xml:lang" + }, + "Instrument.settings": { + "ddi": "form_title: titl; form_id: IDNo; version: verStmt/version; default_language: codeBook/@xml:lang; style: stdyDscr/notes[@type='cdl:setting'][@subject='style']", + "note": "Other settings are not carried." + }, + "Instrument.lists": { + "ddi": "catgry of each var that uses a list" + }, + "Instrument.body": { + "ddi": "dataDscr: varGrp and var, in survey order" + } + } + } + } + ] +} diff --git a/registry/conventions/languageTagging.jsonld b/registry/conventions/languageTagging.jsonld index 01dba75..982a471 100644 --- a/registry/conventions/languageTagging.jsonld +++ b/registry/conventions/languageTagging.jsonld @@ -211,7 +211,9 @@ "qstnLit", "labl", "preQTxt", + "postQTxt", "ivuInstr", + "universe", "notes" ], "baseLanguage": "settings.default_language, else the form's first language; declared as codeBook/@xml:lang whenever the form has more than one language (or default_language is set). Its elements are untagged and come first.", diff --git a/registry/entities/date/ddi.xml b/registry/entities/date/ddi.xml index 7e3fbc7..0345b95 100644 --- a/registry/entities/date/ddi.xml +++ b/registry/entities/date/ddi.xml @@ -22,11 +22,11 @@ - + Wann wurden Sie geboren? Wann wurden Sie geboren? - + diff --git a/registry/entities/decimal/ddi.xml b/registry/entities/decimal/ddi.xml index 99a5211..5ce22b2 100644 --- a/registry/entities/decimal/ddi.xml +++ b/registry/entities/decimal/ddi.xml @@ -22,7 +22,7 @@ - + Wie viele Stunden schlafen Sie durchschnittlich pro Nacht? Wie viele Stunden schlafen Sie durchschnittlich pro Nacht? diff --git a/registry/entities/grid/ddi.xml b/registry/entities/grid/ddi.xml index 6f90bfd..b760b26 100644 --- a/registry/entities/grid/ddi.xml +++ b/registry/entities/grid/ddi.xml @@ -26,7 +26,7 @@ Vertrauen in Institutionen - + Vertrauen in Institutionen Das Parlament @@ -54,7 +54,7 @@ - + Vertrauen in Institutionen Die Polizei diff --git a/registry/entities/integer/ddi.xml b/registry/entities/integer/ddi.xml index c12fc90..cb118cb 100644 --- a/registry/entities/integer/ddi.xml +++ b/registry/entities/integer/ddi.xml @@ -21,8 +21,8 @@ - - + + Wie alt sind Sie? Wie alt sind Sie? diff --git a/registry/entities/integer/definition.jsonld b/registry/entities/integer/definition.jsonld index 2dec3b6..62ea21b 100644 --- a/registry/entities/integer/definition.jsonld +++ b/registry/entities/integer/definition.jsonld @@ -21,7 +21,11 @@ "limesurvey": { "typeCode": "N", "supportsOther": false, - "answerClass": null + "answerClass": null, + "integerOnly": { + "attribute": "num_value_int_only", + "whenWhole": [] + } }, "ddi": { "intrvl": "contin", @@ -44,4 +48,4 @@ "useWhen": "When collecting open integer values without predefined categories, where respondents provide a raw number (e.g. age, household size)." } ] -} \ No newline at end of file +} diff --git a/registry/entities/integer/tsv.tsv b/registry/entities/integer/tsv.tsv index 7e0db34..8ebb44c 100644 --- a/registry/entities/integer/tsv.tsv +++ b/registry/entities/integer/tsv.tsv @@ -1,7 +1,7 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 en en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Untitled Survey en -G Questions 1 en -Q N alter 1 Wie alt sind Sie? en 1 \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 en en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Untitled Survey en +G Questions 1 en +Q N alter 1 Wie alt sind Sie? en 1 1 \ No newline at end of file diff --git a/registry/entities/range/ddi.xml b/registry/entities/range/ddi.xml index 1a6b104..dad2e11 100644 --- a/registry/entities/range/ddi.xml +++ b/registry/entities/range/ddi.xml @@ -22,11 +22,15 @@ - + Wie zufrieden sind Sie insgesamt? (0 = gar nicht, 10 = voll) + + + Wie zufrieden sind Sie insgesamt? (0 = gar nicht, 10 = voll) + step=1 diff --git a/registry/entities/select_multiple/ddi.xml b/registry/entities/select_multiple/ddi.xml index ea209bf..1d9f471 100644 --- a/registry/entities/select_multiple/ddi.xml +++ b/registry/entities/select_multiple/ddi.xml @@ -26,7 +26,7 @@ An welchen Tagen des Wochenendes sind Sie erreichbar? - + An welchen Tagen des Wochenendes sind Sie erreichbar? Samstag @@ -40,7 +40,7 @@ - + An welchen Tagen des Wochenendes sind Sie erreichbar? Sonntag diff --git a/registry/entities/select_multiple_long_list/ddi.xml b/registry/entities/select_multiple_long_list/ddi.xml index 9bb3f85..430aeb6 100644 --- a/registry/entities/select_multiple_long_list/ddi.xml +++ b/registry/entities/select_multiple_long_list/ddi.xml @@ -22,7 +22,7 @@ - + Welche dieser Länder haben Sie bereits besucht? Mehrere Antworten möglich. Welche dieser Länder haben Sie bereits besucht? Mehrere Antworten möglich. diff --git a/registry/entities/select_multiple_other/ddi.xml b/registry/entities/select_multiple_other/ddi.xml index ff917ab..c2c0a81 100644 --- a/registry/entities/select_multiple_other/ddi.xml +++ b/registry/entities/select_multiple_other/ddi.xml @@ -30,7 +30,7 @@ Welche dieser Geräte besitzen Sie? - + Welche dieser Geräte besitzen Sie? Smartphone @@ -44,7 +44,7 @@ - + Welche dieser Geräte besitzen Sie? Laptop @@ -58,7 +58,7 @@ - + Welche dieser Geräte besitzen Sie? Tablet @@ -72,8 +72,9 @@ - + Sonstiges (bitte angeben) + Only if “Welche dieser Geräte besitzen Sie?” includes Sonstiges Sonstiges (bitte angeben) diff --git a/registry/entities/select_one/ddi.xml b/registry/entities/select_one/ddi.xml index 516c8e3..4cfc272 100644 --- a/registry/entities/select_one/ddi.xml +++ b/registry/entities/select_one/ddi.xml @@ -22,7 +22,7 @@ - + Was ist Ihr höchster Bildungsabschluss? diff --git a/registry/entities/select_one_long_list/ddi.xml b/registry/entities/select_one_long_list/ddi.xml index e253587..863b6a2 100644 --- a/registry/entities/select_one_long_list/ddi.xml +++ b/registry/entities/select_one_long_list/ddi.xml @@ -22,7 +22,7 @@ - + In welchem Land wurden Sie geboren? In welchem Land wurden Sie geboren? diff --git a/registry/entities/select_one_other/ddi.xml b/registry/entities/select_one_other/ddi.xml index 3dcd138..4f8844e 100644 --- a/registry/entities/select_one_other/ddi.xml +++ b/registry/entities/select_one_other/ddi.xml @@ -26,7 +26,7 @@ Wie sind Sie auf unser Angebot aufmerksam geworden? - + Wie sind Sie auf unser Angebot aufmerksam geworden? @@ -49,8 +49,9 @@ - + Sonstiges (bitte angeben) + Only if “Wie sind Sie auf unser Angebot aufmerksam geworden?” = Sonstiges Sonstiges (bitte angeben) diff --git a/registry/entities/text/ddi.xml b/registry/entities/text/ddi.xml index 7285630..188307c 100644 --- a/registry/entities/text/ddi.xml +++ b/registry/entities/text/ddi.xml @@ -22,7 +22,7 @@ - + Haben Sie weitere Anmerkungen? Haben Sie weitere Anmerkungen? diff --git a/registry/entities/time/ddi.xml b/registry/entities/time/ddi.xml index e82305b..865b081 100644 --- a/registry/entities/time/ddi.xml +++ b/registry/entities/time/ddi.xml @@ -22,11 +22,11 @@ - + Um wie viel Uhr stehen Sie werktags normalerweise auf? Um wie viel Uhr stehen Sie werktags normalerweise auf? - + diff --git a/src/ddi/codebook.ts b/src/ddi/codebook.ts index e4ce493..63c9396 100644 --- a/src/ddi/codebook.ts +++ b/src/ddi/codebook.ts @@ -23,7 +23,14 @@ import { isGridAppearance } from '../conventions/grid.js'; import { XmlElement } from './xml.js'; import { classifyNotes } from './notes.js'; import { Choice, DdiGroup, Translations, Variable } from './types.js'; -import { joinTranslations, localizedChild, textsOf } from './translations.js'; +import { localizedChild, textsOf } from './translations.js'; +import { + addExclusiveNote, + addFieldNotes, + addGroupFieldNotes, + addSettingNotes, + references, +} from './fields.js'; import { addLogicNotes, addRelevantNote, @@ -93,7 +100,7 @@ interface AddVarSpec { varType: string; choices: Choice[]; opts?: AddVarOpts; - /** The source variable's hint (→ preQTxt) and guidance hint (→ ivuInstr). */ + /** The source variable's hint (→ postQTxt) and guidance hint (→ ivuInstr). */ hint?: string; guidanceHint?: string; /** `label` / `hint` / `guidanceHint` in the form's other languages. */ @@ -107,14 +114,13 @@ interface AddVarSpec { /** An {@link AddVarSpec}'s translations, read off its source variable. */ function specTranslations( v: Variable, - withHint = true, ): Pick< AddVarSpec, 'labelTranslations' | 'hintTranslations' | 'guidanceHintTranslations' > { return { labelTranslations: textsOf(v, 'label'), - ...(withHint ? { hintTranslations: textsOf(v, 'hint') } : {}), + hintTranslations: textsOf(v, 'hint'), guidanceHintTranslations: textsOf(v, 'guidanceHint'), }; } @@ -149,14 +155,6 @@ function categoricalFormat( function addVarElement(parent: XmlElement, spec: AddVarSpec): XmlElement { const { varId, name, label, varType, choices } = spec; const { vocab = '' } = spec.opts ?? {}; - // A folded note or grid lead-in comes first, then the question's own hint. - const preQTxt = [spec.opts?.preQTxt, spec.hint] - .filter((t): t is string => !!t) - .join('\n\n'); - const preQTxtTranslations = joinTranslations( - [spec.opts?.preQTxtTranslations, spec.hintTranslations], - '\n\n', - ); const [intrvl, typeFormat] = DDI_TYPE_MAP[varType] ?? [ 'discrete', 'character', @@ -164,44 +162,113 @@ function addVarElement(parent: XmlElement, spec: AddVarSpec): XmlElement { const respDomain = RESPONSE_DOMAIN_MAP[varType] ?? 'text'; const fmtType = categoricalFormat(typeFormat, respDomain, choices, vocab); - const varEl = parent.child('var', { ID: varId, name, intrvl, files: 'F1' }); - - if (label) { - const qstn = varEl.child('qstn', { responseDomainType: respDomain }); - if (preQTxt) localizedChild(qstn, 'preQTxt', preQTxt, preQTxtTranslations); - localizedChild(qstn, 'qstnLit', label, spec.labelTranslations); - // DDI order within : preQTxt, qstnLit, postQTxt, forward, backward, ivuInstr. - if (spec.guidanceHint) { - localizedChild( - qstn, - 'ivuInstr', - spec.guidanceHint, - spec.guidanceHintTranslations, - ); - } - } + const varEl = parent.child('var', { + ID: varId, + name, + intrvl, + // An integer's data has no decimals; a decimal's number of them is open. + ...(varType === 'integer' ? { dcml: '0' } : {}), + files: 'F1', + }); + + if (label) addQstn(varEl, spec, respDomain); if (spec.logic) { addValrng(varEl, spec.logic.v); addUniverse(varEl, universeOf(spec.logic.v), spec.logic.ctx); } - if (!vocab) { - for (const choice of choices) { - const catgry = varEl.child('catgry'); - catgry.textChild('catValu', choice.name); - localizedChild(catgry, 'labl', choice.label, choice.translations); - } - } + if (!vocab) addCategories(varEl, choices); varEl.textChild('concept', label, vocab ? { vocab } : {}); - varEl.child('varFormat', { type: fmtType, schema: 'other' }); - if (spec.logic) addLogicNotes(varEl, spec.logic.v); + varEl.child('varFormat', { + type: fmtType, + schema: 'other', + ...(DATE_TIME.has(varType) ? { category: varType } : {}), + }); + if (spec.logic) { + addLogicNotes(varEl, spec.logic.v); + addFieldNotes(varEl, spec.logic.v); + } return varEl; } -/** A group-level question's logic on its ``: universe, then notes. */ +/** + * ``: a folded note or a grid's shared text (`preQTxt`), the label, + * then the hints and dependencies (#153). + */ +function addQstn(varEl: XmlElement, spec: AddVarSpec, respDomain: string) { + const qstn = varEl.child('qstn', qstnAttrs(respDomain, spec.logic)); + const preQTxt = spec.opts?.preQTxt; + if (preQTxt) { + localizedChild(qstn, 'preQTxt', preQTxt, spec.opts?.preQTxtTranslations); + } + localizedChild(qstn, 'qstnLit', spec.label, spec.labelTranslations); + addQstnTail(qstn, spec, spec.logic); +} + +function addCategories(varEl: XmlElement, choices: Choice[]): void { + for (const choice of choices) { + const catgry = varEl.child('catgry'); + catgry.textChild('catValu', choice.name); + localizedChild(catgry, 'labl', choice.label, choice.translations); + } +} + +/** Types whose format is a `varFormat/@category` of the same name (#153). */ +const DATE_TIME = new Set(['date', 'time']); + +/** ``'s attributes: its response domain and, known, its position. */ +function qstnAttrs( + respDomain: string, + logic: AddVarSpec['logic'], +): Record { + const seqNo = logic ? logic.ctx.seqNo.get(logic.v.name) : undefined; + return { + responseDomainType: respDomain, + ...(seqNo ? { seqNo: String(seqNo) } : {}), + }; +} + +/** + * `` after `qstnLit`, in XSD order: the hint (`postQTxt`), the + * questions its condition refers to (`backward`), the guidance hint + * (`ivuInstr`). + */ +function addQstnTail( + qstn: XmlElement, + texts: Pick< + AddVarSpec, + 'hint' | 'hintTranslations' | 'guidanceHint' | 'guidanceHintTranslations' + >, + logic: AddVarSpec['logic'], +): void { + if (texts.hint) { + localizedChild(qstn, 'postQTxt', texts.hint, texts.hintTranslations); + } + if (logic) { + const own = logic.ctx.ids.get(logic.v.name); + const refs = references(logic.v.relevant ?? '') + .map((name) => logic.ctx.ids.get(name)) + .filter((id): id is string => !!id && id !== own); + if (refs.length) + qstn.child('backward', { qstn: [...new Set(refs)].join(' ') }); + } + if (texts.guidanceHint) { + localizedChild( + qstn, + 'ivuInstr', + texts.guidanceHint, + texts.guidanceHintTranslations, + ); + } +} + +/** + * A group-level question (a `select_multiple`, a semi-open pair) on its + * ``: universe, any lead-in note, then its typed notes. + */ function addGroupLogic( grpEl: XmlElement, v: Variable, @@ -211,6 +278,8 @@ function addGroupLogic( addUniverse(grpEl, universeOf(v), ctx); if (notes) addGroupNote(grpEl, notes.note, notes.name); addLogicNotes(grpEl, v); + addFieldNotes(grpEl, v); + addExclusiveNote(grpEl, v.choices); } /** Append a binary 0/1 `` for one `select_multiple` option. */ @@ -230,9 +299,20 @@ function addBinaryVar( files: 'F1', }); - const qstn = varEl.child('qstn', { responseDomainType: 'multiple' }); + const logic = { v: question, ctx }; + const qstn = varEl.child('qstn', qstnAttrs('multiple', logic)); localizedChild(qstn, 'preQTxt', questionLabel, textsOf(question, 'label')); localizedChild(qstn, 'qstnLit', choiceLabel, choice.translations); + // The question's hints go with each of its options (#153). + addQstnTail( + qstn, + { + hint: question.hint, + guidanceHint: question.guidanceHint, + ...specTranslations(question), + }, + logic, + ); // The question's notes are on its varGrp; the prose also here, for readers. addUniverse(varEl, universeOf(question), ctx); @@ -446,7 +526,7 @@ export function splitDataVars(dataVars: Variable[]): DataVarBuckets { return { otherPatterns, gridGroups, multiRespGroups, units }; } -/** Emit `` (citation + orphan notes appended). */ +/** Emit `` (citation, then orphan notes and `cdl:setting` notes). */ function addStudyDscr( root: XmlElement, settings: DdiSettings, @@ -474,6 +554,7 @@ function addStudyDscr( if (note.name) attrs.subject = note.name; localizedChild(stdy, 'notes', note.label, textsOf(note, 'label'), attrs); } + addSettingNotes(stdy, settings); } /** Emit `` with `caseQnty` set to the submissions count. */ @@ -553,6 +634,24 @@ function groupTree(units: EmitUnit[]): Map { return tree; } +/** + * The element each question's name refers to: its ``, or for a + * `select_multiple` (plain or semi-open) its ``. + */ +function questionIds(units: EmitUnit[]): Map { + const ids = new Map(); + for (const unit of units) { + if (unit.kind === 'multi') ids.set(unit.v.name, makeGrpId(unit.v.name)); + else if (unit.kind === 'var') ids.set(unit.v.name, makeVarId(unit.v.name)); + else { + const { base, otherVar, isMulti } = unit.p; + ids.set(base.name, isMulti ? makeGrpId(base.name) : makeVarId(base.name)); + ids.set(otherVar.name, makeVarId(otherVar.name)); + } + } + return ids; +} + /** A plain group as `` (#152). */ function addSection( dataDscr: XmlElement, @@ -572,6 +671,7 @@ function addSection( grpEl.textChild('concept', label); addUniverse(grpEl, node.universe, ctx); addRelevantNote(grpEl, group.relevant); + addGroupFieldNotes(grpEl, group, false); } /** Emit every `` element into `` (must come before ``). */ @@ -604,7 +704,10 @@ function addVarGroups( if (node) addUniverse(grpEl, node.universe, ctx); // A lead-in note belongs to the group: a member's preQTxt must equal txt. addGroupNote(grpEl, notes, members[0]?.name ?? ''); - if (node) addRelevantNote(grpEl, node.group.relevant); + if (node) { + addRelevantNote(grpEl, node.group.relevant); + addGroupFieldNotes(grpEl, node.group, true); + } } for (const [smName, smVar] of multiRespGroups) { @@ -647,8 +750,8 @@ function addVars( emitOtherPatternVars(dataDscr, unit.p, ctx); } else if (unit.grid) { const group = getGroupLabel(dataVars, unit.grid); - // preQTxt must equal the group's txt (Schematron), so a member's own - // hint has no slot; validateSubset warns `hint-dropped`. + // preQTxt must equal the group's txt (Schematron); the member's own + // hint is its postQTxt. addVarElement(dataDscr, { varId: makeVarId(unit.v.name), name: unit.v.name, @@ -656,8 +759,9 @@ function addVars( varType: unit.v.type, choices: unit.v.choices, opts: { preQTxt: group.label, preQTxtTranslations: group.translations }, + hint: unit.v.hint, guidanceHint: unit.v.guidanceHint, - ...specTranslations(unit.v, false), + ...specTranslations(unit.v), logic: { v: unit.v, ctx }, }); } else { @@ -733,7 +837,7 @@ export function buildDdiCodebook( const dataDscr = root.child('dataDscr'); const buckets = splitDataVars(dataVars); - const ctx = logicContext(variables, lang); + const ctx = logicContext(variables, lang, questionIds(buckets.units)); addVarGroups(dataDscr, dataVars, notes, buckets, ctx); addVars(dataDscr, dataVars, notes, buckets, ctx); diff --git a/src/ddi/fields.ts b/src/ddi/fields.ts new file mode 100644 index 0000000..ccb4d5d --- /dev/null +++ b/src/ddi/fields.ts @@ -0,0 +1,109 @@ +/** + * The form's remaining fields in the DDI (`convention:ddiFields`, #153): in a + * standard element or attribute where DDI 2.5 has one, else a typed note + * ``. + */ +import conventions from '../generated/conventions.js'; +import { GRID_APPEARANCE } from '../conventions/grid.js'; +import { parseParameters } from '../utils/parameters.js'; +import { TYPE_MAPPINGS } from '../generated/TypeMappings.js'; +import { localizedChild } from './translations.js'; +import type { Choice, DdiGroup, Variable } from './types.js'; +import type { XmlElement } from './xml.js'; + +const NOTES = conventions.conventions.ddiFields.notes; + +/** `range`'s own bounds, which its `valrng` carries. */ +const RANGE_BOUNDS = ['start', 'end']; + +/** + * The `parameters` DDI has no element for, as `key=value` tokens: without + * `guidance_hint` (`ivuInstr`) and a range's `start`/`end` (`valrng`). + */ +export function otherParameters(v: Variable): string { + const tokens: string[] = []; + for (const part of (v.parameters ?? '').split(';')) { + if (/^\s*guidance_hint\s*=/.test(part)) continue; + for (const token of part.split(/[\s,]+/).filter(Boolean)) { + const key = token.split('=')[0].trim().toLowerCase(); + if (v.type === 'range' && RANGE_BOUNDS.includes(key)) continue; + tokens.push(token); + } + } + return tokens.join(' '); +} + +/** A `range`'s bounds, the registry's defaults where not authored. */ +export function rangeBounds(v: Variable): { min: string; max: string } { + const values = { + ...(TYPE_MAPPINGS['range']?.parameters ?? {}), + ...parseParameters(v.parameters ?? ''), + }; + return { min: values['start'], max: values['end'] }; +} + +/** A question's `cdl:default`, `cdl:appearance` and `cdl:parameters`. */ +export function addFieldNotes(el: XmlElement, v: Variable): void { + if (v.default) { + el.textChild('notes', v.default, { type: NOTES.default.type }); + } + if (v.appearance) { + el.textChild('notes', v.appearance, { type: NOTES.appearance.type }); + } + const parameters = otherParameters(v); + if (parameters) { + el.textChild('notes', parameters, { type: NOTES.parameters.type }); + } +} + +/** A `select_multiple`'s exclusive choices as one `cdl:exclusive` note. */ +export function addExclusiveNote(el: XmlElement, choices: Choice[]): void { + const codes = choices.filter((c) => c.exclusive).map((c) => c.name); + if (codes.length) { + el.textChild('notes', codes.join(' '), { type: NOTES.exclusive.type }); + } +} + +/** + * A group's `cdl:hint` (per language) and `cdl:appearance`. A grid's + * `table-list` is its `type="grid"`, so only another appearance is noted. + */ +export function addGroupFieldNotes( + el: XmlElement, + group: DdiGroup, + grid: boolean, +): void { + if (group.hint) { + localizedChild(el, 'notes', group.hint, group.hintTranslations, { + type: NOTES.hint.type, + }); + } + if (group.appearance && !(grid && group.appearance === GRID_APPEARANCE)) { + el.textChild('notes', group.appearance, { type: NOTES.appearance.type }); + } +} + +/** The settings DDI has no element for, as `cdl:setting` notes. */ +export function addSettingNotes( + stdy: XmlElement, + settings: Record, +): void { + for (const key of NOTES.setting.keys) { + const value = settings[key]; + if (typeof value !== 'string' && typeof value !== 'number') continue; + const text = String(value).trim(); + if (text) { + stdy.textChild('notes', text, { + type: NOTES.setting.type, + subject: key, + }); + } + } +} + +/** The `${name}`s an expression refers to, in order, each once. */ +export function references(expression: string): string[] { + return [ + ...new Set([...expression.matchAll(/\$\{([^}]+)\}/g)].map((m) => m[1])), + ]; +} diff --git a/src/ddi/fromInstrument.ts b/src/ddi/fromInstrument.ts index 4ba94a0..42e4692 100644 --- a/src/ddi/fromInstrument.ts +++ b/src/ddi/fromInstrument.ts @@ -15,6 +15,7 @@ import { otherLabelFor, } from '../conventions/other.js'; import { isFromFileType, vocabFromFilename } from '../conventions/fromFile.js'; +import { isExclusive } from '../conventions/exclusive.js'; import { TYPE_MAP, NON_DDI_EMITTABLE_TYPES, @@ -123,13 +124,19 @@ function groupsOf(ctx: GroupContext): Pick { return ctx.groups.length ? { groups: ctx.groups } : {}; } -/** A question's own logic fields, absent when empty (#151). */ +/** A question's own logic (#151) and form fields (#153), absent when empty. */ function logicOf( q: QuestionItem, lang: string | undefined, ): Pick< Variable, - 'relevant' | 'constraint' | 'constraintMessage' | 'required' + | 'relevant' + | 'constraint' + | 'constraintMessage' + | 'required' + | 'default' + | 'appearance' + | 'parameters' > { const relevant = q.relevant.trim(); const message = pick(q.constraintMessage, lang).trim(); @@ -138,6 +145,9 @@ function logicOf( ...(q.constraint ? { constraint: q.constraint } : {}), ...(q.constraint && message ? { constraintMessage: message } : {}), ...(q.required ? { required: true } : {}), + ...(q.default ? { default: q.default } : {}), + ...(q.appearance ? { appearance: q.appearance } : {}), + ...(q.parameters.trim() ? { parameters: q.parameters.trim() } : {}), }; } @@ -371,6 +381,7 @@ function project(items: Item[], ctx: GroupContext, state: ProjectState): void { const path = ctx.path ? `${ctx.path}/${item.name}` : item.name; const label = pick(item.label, state.lang); const translations = translationsOf(item.label, state.others); + const hintTranslations = translationsOf(item.hint, state.others); const group: DdiGroup = { name: item.name, path, @@ -378,6 +389,8 @@ function project(items: Item[], ctx: GroupContext, state: ProjectState): void { ...(translations ? { translations } : {}), appearance: item.appearance, relevant: item.relevant.trim(), + hint: pick(item.hint, state.lang).trim(), + ...(hintTranslations ? { hintTranslations } : {}), }; project( item.children, @@ -474,6 +487,7 @@ export function choicesFromInstrument( name: c.name, label: pick(c.label, lang), ...(translations ? { translations } : {}), + ...(isExclusive(c.row) ? { exclusive: true } : {}), }; }), ]), diff --git a/src/ddi/logic.ts b/src/ddi/logic.ts index e1431c2..c2168a8 100644 --- a/src/ddi/logic.ts +++ b/src/ddi/logic.ts @@ -11,6 +11,7 @@ */ import conventions from '../generated/conventions.js'; import { parseXPath, type XPathNode } from '../instrument/xpathParser.js'; +import { rangeBounds } from './fields.js'; import { localizedChild } from './translations.js'; import type { Choice, Variable } from './types.js'; import type { XmlElement } from './xml.js'; @@ -39,12 +40,17 @@ export interface LogicContext { base: string | null; /** The codebook's other language tags. */ others: string[]; + /** The `` or `` ID a `${name}` refers to (#153). */ + ids: Map; + /** Each data question's position in the form, from 1 (`qstn/@seqNo`). */ + seqNo: Map; } /** The languages a variable's texts come in: its base and translations. */ export function logicContext( variables: Variable[], base: string | null, + ids: Map = new Map(), ): LogicContext { const others = new Set(); for (const v of variables) { @@ -54,6 +60,12 @@ export function logicContext( byName: new Map(variables.map((v) => [v.name, v])), base, others: [...others].filter((t) => t !== base), + ids, + seqNo: new Map( + variables + .filter((v) => v.type !== 'note') + .map((v, i): [string, number] => [v.name, i + 1]), + ), }; } @@ -337,12 +349,17 @@ export function simpleRange(constraint: string): Range | null { return range; } -const NUMERIC_TYPES = new Set(['integer', 'decimal']); +const NUMERIC_TYPES = new Set(['integer', 'decimal', 'range']); -/** `` for a numeric variable's simple constraint. */ +/** + * ``: a numeric variable's simple constraint, else + * a `range`'s `start`/`end` (#153). + */ export function addValrng(el: XmlElement, v: Variable): void { - if (!v.constraint || !NUMERIC_TYPES.has(v.type)) return; - const range = simpleRange(v.constraint); + const range = + (v.constraint && NUMERIC_TYPES.has(v.type) + ? simpleRange(v.constraint) + : null) ?? (v.type === 'range' ? rangeBounds(v) : null); if (range) el.child('valrng').child('range', range); } diff --git a/src/ddi/types.ts b/src/ddi/types.ts index ae82fc3..feeb9db 100644 --- a/src/ddi/types.ts +++ b/src/ddi/types.ts @@ -19,6 +19,8 @@ export interface Choice { label: string; /** `label` in the form's other languages. */ translations?: Translations; + /** Marked `exclusive` (convention:exclusiveChoice): a `cdl:exclusive` note. */ + exclusive?: boolean; } /** @@ -37,6 +39,10 @@ export interface DdiGroup { appearance: string; /** Its own `relevant` (XPath); `''` when none. */ relevant: string; + /** Its `hint`: a `cdl:hint` note (#153); `''` when none. */ + hint: string; + /** `hint` in the form's other languages. */ + hintTranslations?: Translations; } /** A variable's texts in one of the form's other languages. */ @@ -68,7 +74,7 @@ export interface Variable { vocab: string; /** Resolved answer options; empty for non-categorical / external-list types. */ choices: Choice[]; - /** XLSForm `hint`, emitted as `` (after any folded note). */ + /** XLSForm `hint`, emitted as `` (#153). */ hint?: string; /** XLSForm `guidance_hint`, emitted as ``. */ guidanceHint?: string; @@ -88,6 +94,12 @@ export interface Variable { constraintMessage?: string; /** XLSForm `required`: a `cdl:required` note. */ required?: boolean; + /** XLSForm `default`: a `cdl:default` note (#153). */ + default?: string; + /** The question's own lowercased `appearance`: a `cdl:appearance` note. */ + appearance?: string; + /** XLSForm `parameters` as authored (#153). */ + parameters?: string; /** Its texts in the form's other languages, by language tag (#135). */ translations?: Record; } diff --git a/src/generated/TypeMappings.ts b/src/generated/TypeMappings.ts index 0c74955..9edf243 100644 --- a/src/generated/TypeMappings.ts +++ b/src/generated/TypeMappings.ts @@ -59,11 +59,13 @@ export const TYPE_MAPPINGS: Record = { kind: "question", limeSurveyType: "N", supported: true, + integerOnly: {"attribute": "num_value_int_only", "whenWhole": []}, }, int: { kind: "question", limeSurveyType: "N", supported: true, + integerOnly: {"attribute": "num_value_int_only", "whenWhole": []}, }, note: { kind: "question", diff --git a/src/generated/conventions.json b/src/generated/conventions.json index c0d86ec..8ec8126 100644 --- a/src/generated/conventions.json +++ b/src/generated/conventions.json @@ -1,6 +1,166 @@ { "$comment": "GENERATED - DO NOT EDIT. Source: registry/root.jsonld", "conventions": { + "ddiFields": { + "description": "Where each field of a form (the Instrument model, src/instrument/types.ts) goes in a CDL DDI codebook (formtransform#153): a standard DDI 2.5 element or attribute wherever one fits, a typed note `\">` (the XSD's controlled-vocabulary extension point, as in convention:logicMapping) only where DDI 2.5 has none, or a documented loss. A unit test fails when a model field has no entry.", + "questionText": { + "preQTxt": "a note row before the question, or a grid's or select_multiple's shared question text", + "qstnLit": "the label", + "postQTxt": "the hint shown to respondents with the question", + "ivuInstr": "guidance_hint (for interviewers and the codebook, not shown)", + "order": "preQTxt, qstnLit, postQTxt, backward, ivuInstr (XSD order)" + }, + "backward": "qstn/backward/@qstn lists the IDs of the questions its own relevant refers to (a var, or the varGrp of a select_multiple or other pair).", + "seqNo": "qstn/@seqNo is the question's position among the form's data questions, from 1; a select_multiple's binaries share their question's.", + "notes": { + "default": { + "type": "cdl:default", + "on": [ + "var", + "varGrp" + ], + "text": "the default cell as authored" + }, + "appearance": { + "type": "cdl:appearance", + "on": [ + "var", + "varGrp" + ], + "text": "the appearance cell, lowercased" + }, + "parameters": { + "type": "cdl:parameters", + "on": [ + "var", + "varGrp" + ], + "text": "the parameters not in a standard element, space-separated key=value" + }, + "hint": { + "type": "cdl:hint", + "on": [ + "varGrp[@type='section' or @type='grid']" + ], + "localized": true + }, + "exclusive": { + "type": "cdl:exclusive", + "on": [ + "varGrp[@type='multipleResp']" + ], + "text": "the exclusive choices' codes, space-separated" + }, + "setting": { + "type": "cdl:setting", + "on": [ + "stdyDscr" + ], + "subject": "the settings key", + "keys": [ + "style" + ] + } + }, + "fields": { + "ItemBase.name": { + "ddi": "var/@name; a group's path in varGrp/@name" + }, + "ItemBase.label": { + "ddi": "qstn/qstnLit; a group's varGrp/txt" + }, + "ItemBase.hint": { + "ddi": "qstn/postQTxt; a group's in a cdl:hint note (varGrp has no hint element)", + "note": "CDL uses postQTxt for the hint shown to respondents with the question, though the XSD describes it as text about what follows the question: DDI 2.5 has no hint element." + }, + "ItemBase.relevant": { + "ddi": "universe + cdl:relevant note (convention:logicMapping)" + }, + "ItemBase.appearance": { + "cdlNote": "cdl:appearance", + "note": "On a var (a select_multiple's or pair's on its varGrp), and on a section; a grid's table-list is its type='grid', so only a grid appearance other than that is noted." + }, + "ItemBase.row": { + "loss": "Columns the model does not lift (media, calculation, …) are not carried." + }, + "GroupItem.kind": { + "ddi": "a group is a varGrp" + }, + "GroupItem.children": { + "ddi": "varGrp/@var and @varGrp (nesting), var element order and qstn/@seqNo (order)" + }, + "GroupItem.closed": { + "loss": "Every group read back is closed." + }, + "QuestionItem.kind": { + "ddi": "a question is a var (a select_multiple: a varGrp of binary vars)" + }, + "QuestionItem.type": { + "ddi": "qstn/@responseDomainType, var/@intrvl, varFormat/@type, varFormat/@category (date, time), var/@dcml='0' (integer)" + }, + "QuestionItem.rawType": { + "loss": "Rebuilt from type, list and the other pattern; the or_other shorthand comes back as the explicit pair, which behaves the same." + }, + "QuestionItem.list": { + "loss": "The list name: questions with identical category sets come back sharing one list." + }, + "QuestionItem.file": { + "ddi": "concept/@vocab (the file is .csv)" + }, + "QuestionItem.orOther": { + "ddi": "varGrp[@type='other'] (convention:other)", + "note": "Comes back as the explicit pair, see rawType." + }, + "QuestionItem.guidanceHint": { + "ddi": "qstn/ivuInstr" + }, + "QuestionItem.constraint": { + "ddi": "cdl:constraint note, plus valrng/range for a simple numeric range (convention:logicMapping)" + }, + "QuestionItem.constraintMessage": { + "cdlNote": "cdl:constraint_message", + "note": "One per language (convention:logicMapping)." + }, + "QuestionItem.required": { + "cdlNote": "cdl:required" + }, + "QuestionItem.default": { + "cdlNote": "cdl:default" + }, + "QuestionItem.parameters": { + "ddi": "range start/end: valrng/range @min/@max; guidance_hint: qstn/ivuInstr; the other parameters (step, randomize, …) in a cdl:parameters note" + }, + "QuestionItem.otherLabel": { + "ddi": "the other answer's catgry/labl and the companion's qstnLit" + }, + "InstrumentChoice.name": { + "ddi": "catgry/catValu" + }, + "InstrumentChoice.label": { + "ddi": "catgry/labl, one per language" + }, + "InstrumentChoice.row": { + "ddi": "exclusive: a cdl:exclusive note on the select_multiple's varGrp listing the codes", + "note": "The choice sheet's other columns are not carried." + }, + "Instrument.languages": { + "ddi": "xml:lang siblings of every text (convention:languageTagging)" + }, + "Instrument.defaultLanguage": { + "ddi": "codeBook/@xml:lang" + }, + "Instrument.settings": { + "ddi": "form_title: titl; form_id: IDNo; version: verStmt/version; default_language: codeBook/@xml:lang; style: stdyDscr/notes[@type='cdl:setting'][@subject='style']", + "note": "Other settings are not carried." + }, + "Instrument.lists": { + "ddi": "catgry of each var that uses a list" + }, + "Instrument.body": { + "ddi": "dataDscr: varGrp and var, in survey order" + } + } + }, "exclusiveChoice": { "choicesColumn": "exclusive", "trueValues": [ @@ -214,7 +374,9 @@ "qstnLit", "labl", "preQTxt", + "postQTxt", "ivuInstr", + "universe", "notes" ], "baseLanguage": "settings.default_language, else the form's first language; declared as codeBook/@xml:lang whenever the form has more than one language (or default_language is set). Its elements are untagged and come first.", diff --git a/src/generated/conventions.ts b/src/generated/conventions.ts index e507ee1..5786e0f 100644 --- a/src/generated/conventions.ts +++ b/src/generated/conventions.ts @@ -10,6 +10,166 @@ const conventions = { "$comment": "GENERATED - DO NOT EDIT. Source: registry/root.jsonld", "conventions": { + "ddiFields": { + "description": "Where each field of a form (the Instrument model, src/instrument/types.ts) goes in a CDL DDI codebook (formtransform#153): a standard DDI 2.5 element or attribute wherever one fits, a typed note `\">` (the XSD's controlled-vocabulary extension point, as in convention:logicMapping) only where DDI 2.5 has none, or a documented loss. A unit test fails when a model field has no entry.", + "questionText": { + "preQTxt": "a note row before the question, or a grid's or select_multiple's shared question text", + "qstnLit": "the label", + "postQTxt": "the hint shown to respondents with the question", + "ivuInstr": "guidance_hint (for interviewers and the codebook, not shown)", + "order": "preQTxt, qstnLit, postQTxt, backward, ivuInstr (XSD order)" + }, + "backward": "qstn/backward/@qstn lists the IDs of the questions its own relevant refers to (a var, or the varGrp of a select_multiple or other pair).", + "seqNo": "qstn/@seqNo is the question's position among the form's data questions, from 1; a select_multiple's binaries share their question's.", + "notes": { + "default": { + "type": "cdl:default", + "on": [ + "var", + "varGrp" + ], + "text": "the default cell as authored" + }, + "appearance": { + "type": "cdl:appearance", + "on": [ + "var", + "varGrp" + ], + "text": "the appearance cell, lowercased" + }, + "parameters": { + "type": "cdl:parameters", + "on": [ + "var", + "varGrp" + ], + "text": "the parameters not in a standard element, space-separated key=value" + }, + "hint": { + "type": "cdl:hint", + "on": [ + "varGrp[@type='section' or @type='grid']" + ], + "localized": true + }, + "exclusive": { + "type": "cdl:exclusive", + "on": [ + "varGrp[@type='multipleResp']" + ], + "text": "the exclusive choices' codes, space-separated" + }, + "setting": { + "type": "cdl:setting", + "on": [ + "stdyDscr" + ], + "subject": "the settings key", + "keys": [ + "style" + ] + } + }, + "fields": { + "ItemBase.name": { + "ddi": "var/@name; a group's path in varGrp/@name" + }, + "ItemBase.label": { + "ddi": "qstn/qstnLit; a group's varGrp/txt" + }, + "ItemBase.hint": { + "ddi": "qstn/postQTxt; a group's in a cdl:hint note (varGrp has no hint element)", + "note": "CDL uses postQTxt for the hint shown to respondents with the question, though the XSD describes it as text about what follows the question: DDI 2.5 has no hint element." + }, + "ItemBase.relevant": { + "ddi": "universe + cdl:relevant note (convention:logicMapping)" + }, + "ItemBase.appearance": { + "cdlNote": "cdl:appearance", + "note": "On a var (a select_multiple's or pair's on its varGrp), and on a section; a grid's table-list is its type='grid', so only a grid appearance other than that is noted." + }, + "ItemBase.row": { + "loss": "Columns the model does not lift (media, calculation, …) are not carried." + }, + "GroupItem.kind": { + "ddi": "a group is a varGrp" + }, + "GroupItem.children": { + "ddi": "varGrp/@var and @varGrp (nesting), var element order and qstn/@seqNo (order)" + }, + "GroupItem.closed": { + "loss": "Every group read back is closed." + }, + "QuestionItem.kind": { + "ddi": "a question is a var (a select_multiple: a varGrp of binary vars)" + }, + "QuestionItem.type": { + "ddi": "qstn/@responseDomainType, var/@intrvl, varFormat/@type, varFormat/@category (date, time), var/@dcml='0' (integer)" + }, + "QuestionItem.rawType": { + "loss": "Rebuilt from type, list and the other pattern; the or_other shorthand comes back as the explicit pair, which behaves the same." + }, + "QuestionItem.list": { + "loss": "The list name: questions with identical category sets come back sharing one list." + }, + "QuestionItem.file": { + "ddi": "concept/@vocab (the file is .csv)" + }, + "QuestionItem.orOther": { + "ddi": "varGrp[@type='other'] (convention:other)", + "note": "Comes back as the explicit pair, see rawType." + }, + "QuestionItem.guidanceHint": { + "ddi": "qstn/ivuInstr" + }, + "QuestionItem.constraint": { + "ddi": "cdl:constraint note, plus valrng/range for a simple numeric range (convention:logicMapping)" + }, + "QuestionItem.constraintMessage": { + "cdlNote": "cdl:constraint_message", + "note": "One per language (convention:logicMapping)." + }, + "QuestionItem.required": { + "cdlNote": "cdl:required" + }, + "QuestionItem.default": { + "cdlNote": "cdl:default" + }, + "QuestionItem.parameters": { + "ddi": "range start/end: valrng/range @min/@max; guidance_hint: qstn/ivuInstr; the other parameters (step, randomize, …) in a cdl:parameters note" + }, + "QuestionItem.otherLabel": { + "ddi": "the other answer's catgry/labl and the companion's qstnLit" + }, + "InstrumentChoice.name": { + "ddi": "catgry/catValu" + }, + "InstrumentChoice.label": { + "ddi": "catgry/labl, one per language" + }, + "InstrumentChoice.row": { + "ddi": "exclusive: a cdl:exclusive note on the select_multiple's varGrp listing the codes", + "note": "The choice sheet's other columns are not carried." + }, + "Instrument.languages": { + "ddi": "xml:lang siblings of every text (convention:languageTagging)" + }, + "Instrument.defaultLanguage": { + "ddi": "codeBook/@xml:lang" + }, + "Instrument.settings": { + "ddi": "form_title: titl; form_id: IDNo; version: verStmt/version; default_language: codeBook/@xml:lang; style: stdyDscr/notes[@type='cdl:setting'][@subject='style']", + "note": "Other settings are not carried." + }, + "Instrument.lists": { + "ddi": "catgry of each var that uses a list" + }, + "Instrument.body": { + "ddi": "dataDscr: varGrp and var, in survey order" + } + } + }, "exclusiveChoice": { "choicesColumn": "exclusive", "trueValues": [ @@ -223,7 +383,9 @@ const conventions = { "qstnLit", "labl", "preQTxt", + "postQTxt", "ivuInstr", + "universe", "notes" ], "baseLanguage": "settings.default_language, else the form's first language; declared as codeBook/@xml:lang whenever the form has more than one language (or default_language is set). Its elements are untagged and come first.", diff --git a/src/instrument/lstsvTypes.ts b/src/instrument/lstsvTypes.ts index bc42a83..159d9da 100644 --- a/src/instrument/lstsvTypes.ts +++ b/src/instrument/lstsvTypes.ts @@ -67,6 +67,14 @@ function resolveParameterized( } const CANONICAL_NUMERIC_TYPE = canonicalTypeFor('N'); +/** + * The type LimeSurvey's integer-only numeric question is, with the flag's + * attribute: a parameterless type whose `integerOnly` is always set. + */ +const INTEGER_TYPE: [string, string] | undefined = Object.entries(TYPE_MAPPINGS) + .filter(([, m]) => m.limeSurveyType === 'N' && m.integerOnly && !m.parameters) + .map(([t, m]): [string, string] => [t, m.integerOnly!.attribute])[0]; + /** Appearance name → LS type-override code it produces (e.g. `minimal` → `!`). */ const APPEARANCE_BY_OVERRIDE: Record = {}; for (const [name, spec] of Object.entries(APPEARANCES)) { @@ -97,7 +105,12 @@ export function resolveType(lsCode: string, row: Row): ResolvedType { return { base: 'select_multiple' }; case 'N': return ( - resolveParameterized(lsCode, row) ?? { base: CANONICAL_NUMERIC_TYPE } + resolveParameterized(lsCode, row) ?? { + base: + INTEGER_TYPE && cell(row, INTEGER_TYPE[1]) === '1' + ? INTEGER_TYPE[0] + : CANONICAL_NUMERIC_TYPE, + } ); case 'D': return { diff --git a/src/pipelines/README.md b/src/pipelines/README.md index 16976b4..f5bb763 100644 --- a/src/pipelines/README.md +++ b/src/pipelines/README.md @@ -58,8 +58,7 @@ The plan to carry all of it is #155 (#151–#154). The canonical `Variable` (`src/ddi/types.ts`) is what survives an emit: `name`, `type`, `label`, group path/label/appearance, `listName`, `vocab`, `choices`, -the question's `hint` (``) and `guidance_hint` (``), and -since #151 its logic: +and since #151–#153 the rest of the form: - **`relevant`, `constraint`, `constraint_message`, `required`.** DDI Codebook 2.5 has no expression syntax, so each is written twice: readable @@ -72,8 +71,13 @@ since #151 its logic: - **groups and order** (#152): every group is a `` (a plain one `type="section"`), nested through `@varGrp`, and the ``s follow the survey. -- **still absent:** `default`, per-question `appearance`, `calculation`, the - group hint, and a note row apart from the hint after it (#153). +- **every other field** (#153): standard DDI where it has a home (`postQTxt` + for the hint, `ivuInstr` for `guidance_hint`, a note row as `preQTxt`, + `varFormat/@category`, `var/@dcml`, `valrng`, `qstn/@seqNo`, + `qstn/backward`), else a typed note. `convention:ddiFields` + ([`registry/conventions/ddiFields.jsonld`](../../registry/conventions/ddiFields.jsonld)) + maps every model field, or names it a loss: `calculation` and other + unlifted columns, a list's name, the `or_other` shorthand as such. Compare `lstsv2xlsform`, which *is* implemented: a LimeSurvey structure TSV carries `relevance`, `em_validation_q`, `mandatory`, `default` and the `!`/`T` @@ -81,9 +85,9 @@ type overrides. It is a form definition in a different dialect, so reversing it is a translation problem. Reversing a codebook without those pieces is a *reconstruction* problem, and they cannot be inferred from it. -The failure mode matters more than the missing feature. A `ddi2xlsform` before -the rest of #155 would emit a survey that looks correct and behaves wrongly: no -defaults, no appearances. Silently producing a broken instrument is worse +The failure mode matters more than the missing feature. A `ddi2xlsform` that +reads a codebook without the CDL notes (someone else's, or one from before +formtransform#151–#153) emits a survey that looks correct and behaves wrongly. Silently producing a broken instrument is worse than declining to produce one — the same reasoning behind `validateLstsvSubset` rejecting out-of-subset input rather than guessing at it. diff --git a/src/pipelines/lstsv2xlsform/README.md b/src/pipelines/lstsv2xlsform/README.md index 25d50b2..4cf3838 100644 --- a/src/pipelines/lstsv2xlsform/README.md +++ b/src/pipelines/lstsv2xlsform/README.md @@ -90,11 +90,11 @@ too). These are the fields where a round-trip is **not** byte-exact. Anything not listed here must match exactly. -1. **`N` → `integer` vs `decimal` — unrecoverable.** Both map to LimeSurvey `N` - with no distinguishing attribute anywhere (`TYPE_MAPPINGS` entries are - identical bar the key). No signal exists to read back without changing the - LimeSurvey side. `lstsvRowsToXlsform` emits the canonical `decimal`; - `integer` fixtures round-trip with `type` differing. +1. **`N` → `integer` vs `decimal`.** Both are LimeSurvey `N`; an `integer` + also sets `num_value_int_only=1` (LimeSurvey's "only integer values", the + registry's `integerOnly`), so an `N` with it reads back as `integer` and one + without as `decimal`. A decimal question someone set to integer-only in + LimeSurvey comes back as `integer`, which is what it accepts. An `N` with both bound attributes is read back as `range` (the only type that writes them). Its `step` is not stored: `num_value_int_only=1` comes diff --git a/src/pipelines/xlsform2lstsv/parameters.ts b/src/pipelines/xlsform2lstsv/parameters.ts index 9b8a93f..8eaef6c 100644 --- a/src/pipelines/xlsform2lstsv/parameters.ts +++ b/src/pipelines/xlsform2lstsv/parameters.ts @@ -6,17 +6,18 @@ */ import { TYPE_MAPPINGS } from '../../generated/TypeMappings.js'; import { ConversionError } from '../../diagnostics.js'; +import { parseParameters } from '../../utils/parameters.js'; -/** Parse `key=value` pairs separated by spaces, commas or semicolons. */ -export function parseParameters(cell: unknown): Record { - const out: Record = {}; - if (typeof cell !== 'string' && typeof cell !== 'number') return out; - for (const token of String(cell).split(/[\s,;]+/)) { - const eq = token.indexOf('='); - if (eq <= 0) continue; - out[token.slice(0, eq).trim().toLowerCase()] = token.slice(eq + 1).trim(); - } - return out; +export { parseParameters }; + +/** ODK allows a descending range (start > end); LimeSurvey only has bounds. */ +function ascending(values: Record): void { + if (!('start' in values && 'end' in values)) return; + const [lo, hi] = [Number(values.start), Number(values.end)].sort( + (a, b) => a - b, + ); + values.start = String(lo); + values.end = String(hi); } const isNumber = (v: string) => v !== '' && Number.isFinite(Number(v)); @@ -32,6 +33,10 @@ export function parameterAttributes( questionName: string, ): Record { const mapping = TYPE_MAPPINGS[baseType]; + // An integer is LimeSurvey's numeric question accepting whole numbers only. + if (mapping?.integerOnly && !mapping.parameters) { + return { [mapping.integerOnly.attribute]: '1' }; + } if (!mapping?.parameters) return {}; const values = { ...mapping.parameters, ...parseParameters(cell) }; @@ -51,14 +56,7 @@ export function parameterAttributes( `${baseType} '${questionName}': step must not be 0`, ); } - // ODK allows a descending range (start > end); LimeSurvey only has bounds. - if ('start' in values && 'end' in values) { - const [lo, hi] = [Number(values.start), Number(values.end)].sort( - (a, b) => a - b, - ); - values.start = String(lo); - values.end = String(hi); - } + ascending(values); const attrs: Record = {}; for (const [key, attr] of Object.entries(mapping.parameterAttributes ?? {})) { diff --git a/src/utils/parameters.ts b/src/utils/parameters.ts new file mode 100644 index 0000000..ac7c535 --- /dev/null +++ b/src/utils/parameters.ts @@ -0,0 +1,13 @@ +/** The XLSForm `parameters` cell. */ + +/** Parse `key=value` pairs separated by spaces, commas or semicolons. */ +export function parseParameters(cell: unknown): Record { + const out: Record = {}; + if (typeof cell !== 'string' && typeof cell !== 'number') return out; + for (const token of String(cell).split(/[\s,;]+/)) { + const eq = token.indexOf('='); + if (eq <= 0) continue; + out[token.slice(0, eq).trim().toLowerCase()] = token.slice(eq + 1).trim(); + } + return out; +} diff --git a/src/xlsform/validate.ts b/src/xlsform/validate.ts index 9cbac83..bafd4e8 100644 --- a/src/xlsform/validate.ts +++ b/src/xlsform/validate.ts @@ -19,7 +19,6 @@ import { import { OTHER_SUFFIX, otherCompanionBase } from '../conventions/other.js'; import { isFromFileType } from '../conventions/fromFile.js'; import { METADATA_ROW_TYPES } from '../conventions/metadata.js'; -import { isGridAppearance } from '../conventions/grid.js'; import { normalizeName } from './identifiers.js'; import { expressionReferenceDiagnostics } from './references.js'; @@ -526,18 +525,11 @@ export class XLSValidator { fileChoices: options.fileChoices, target, }; - // Appearance of each open group, innermost last: a table-list group is a - // DDI grid, whose members have no text slot of their own. - const groups: string[] = []; for (const row of surveyData) { const problem = this.rowDiagnostic(row, ctx); if (problem) violations.push(problem); this.collectAppearanceViolations(row, violations); - const type = (row.type || '').trim(); - if (/^begin[_ ]group$/.test(type)) groups.push(cellText(row.appearance)); - else if (/^end[_ ]group$/.test(type)) groups.pop(); - const inGrid = isGridAppearance(groups[groups.length - 1] ?? ''); - const dropped = this.droppedHint(row, target, inGrid); + const dropped = this.droppedHint(row, target); if (dropped) violations.push(dropped); } @@ -583,16 +575,13 @@ export class XLSValidator { } /** - * A hint the target has no place for. DDI: a `select_multiple` becomes a - * group of binary variables with no per-question text, so its `hint` and - * `guidance_hint` are lost; a grid member's preQTxt must equal the grid's - * text, so its `hint` is lost. LimeSurvey: `guidance_hint` has no - * equivalent (`hint` becomes the question's help text). + * A hint the target has no place for: LimeSurvey has no equivalent of + * `guidance_hint` (`hint` becomes the question's help text). DDI keeps + * both on every question (#153): `postQTxt` and `ivuInstr`. */ private static droppedHint( row: SurveyRow, target: SubsetTarget, - inGrid = false, ): Diagnostic | null { const has = (col: string) => Object.entries(row).some( @@ -601,24 +590,11 @@ export class XLSValidator { ) || (col === 'guidance_hint' && /(^|;)\s*guidance_hint\s*=/.test(cellText(row['parameters']))); - const baseType = (row.type || '').trim().split(/\s+/)[0]; const name = typeof row.name === 'string' ? row.name.trim() : ''; - let lost: string[] = []; - let why = ''; - if (target === 'ddi' && baseType === 'select_multiple') { - lost = ['hint', 'guidance_hint'].filter(has); - why = 'a select_multiple has no per-question text slot in DDI'; - } else if (target === 'ddi' && inGrid) { - lost = ['hint'].filter(has); - why = "a grid member's question text is the grid's in DDI"; - } else if (target === 'lstsv') { - lost = ['guidance_hint'].filter(has); - why = 'LimeSurvey has no equivalent'; - } - if (lost.length === 0) return null; + if (target !== 'lstsv' || !has('guidance_hint')) return null; return warning( 'hint-dropped', - `${lost.join(' and ')} on "${name}" is not carried over: ${why}`, + `guidance_hint on "${name}" is not carried over: LimeSurvey has no equivalent`, name, ); } diff --git a/tests/fixtures/surveys/all_types_survey/ddi.xml b/tests/fixtures/surveys/all_types_survey/ddi.xml index e65ad8b..275700d 100644 --- a/tests/fixtures/surveys/all_types_survey/ddi.xml +++ b/tests/fixtures/surveys/all_types_survey/ddi.xml @@ -39,6 +39,7 @@ Programmierkenntnisse Programmierkenntnisse + field-list Weitere Fragen @@ -65,63 +66,63 @@ Welche Tracks würden Sie erneut besuchen? - + Wie lautet Ihre Teilnehmer-ID? Wie lautet Ihre Teilnehmer-ID? - + Wie heißen Sie (Vor- und Nachname)? Wie heißen Sie (Vor- und Nachname)? - + Wie lautet Ihre E-Mail-Adresse? Wie lautet Ihre E-Mail-Adresse? - - + + An wie vielen Workshops haben Sie bisher teilgenommen? An wie vielen Workshops haben Sie bisher teilgenommen? - - + + An wie vielen Sessions haben Sie heute teilgenommen? An wie vielen Sessions haben Sie heute teilgenommen? - + Wie bewerten Sie den Workshop auf einer Skala von 1,0 bis 10,0? Wie bewerten Sie den Workshop auf einer Skala von 1,0 bis 10,0? - + Wann sind Sie geboren? Wann sind Sie geboren? - + - + Um wie viel Uhr sind Sie angekommen? Um wie viel Uhr sind Sie angekommen? - + - + Würden Sie diesen Workshop weiterempfehlen? @@ -136,7 +137,7 @@ - + An welchen Tracks haben Sie teilgenommen? Frontend-Entwicklung @@ -150,7 +151,7 @@ - + An welchen Tracks haben Sie teilgenommen? Backend-Entwicklung @@ -164,7 +165,7 @@ - + An welchen Tracks haben Sie teilgenommen? Data Science @@ -178,21 +179,23 @@ - + Beschreiben Sie bitte Ihre Erfahrungen beim Workshop Beschreiben Sie bitte Ihre Erfahrungen beim Workshop + multiline - + Welche Verbesserungsvorschläge haben Sie für künftige Veranstaltungen? Welche Verbesserungsvorschläge haben Sie für künftige Veranstaltungen? + multiline - + Wie zufrieden waren Sie mit der Organisation insgesamt? @@ -209,9 +212,10 @@ Wie zufrieden waren Sie mit der Organisation insgesamt? + likert - + Bevorzugst du Dropdown? @@ -224,9 +228,10 @@ Bevorzugst du Dropdown? + minimal - + Python @@ -247,9 +252,10 @@ Python + list-nolabel - + JavaScript @@ -270,9 +276,10 @@ JavaScript + list-nolabel - + SQL @@ -293,9 +300,10 @@ SQL + list-nolabel - + Welcher Track hat Ihnen am besten gefallen? @@ -318,8 +326,9 @@ - + Sonstiges: + Nur wenn „Welcher Track hat Ihnen am besten gefallen?“ = Sonstiges: Sonstiges: @@ -327,7 +336,7 @@ ${q_sel1_other} = 'other' - + Welche Tracks würden Sie erneut besuchen? Frontend-Entwicklung @@ -341,7 +350,7 @@ - + Welche Tracks würden Sie erneut besuchen? Backend-Entwicklung @@ -355,7 +364,7 @@ - + Welche Tracks würden Sie erneut besuchen? Data Science @@ -369,8 +378,9 @@ - + Sonstiges: + Nur wenn „Welche Tracks würden Sie erneut besuchen?“ enthält Sonstiges: Sonstiges: @@ -378,7 +388,7 @@ selected(${q_selm_other}, 'other') - + Wie lautet Ihre E-Mail-Adresse? Wie lautet Ihre E-Mail-Adresse? @@ -386,15 +396,15 @@ yes - - Format: +49 123 456789 + Wie lautet Ihre Telefonnummer? + Format: +49 123 456789 Wie lautet Ihre Telefonnummer? - - + + Wie alt sind Sie? @@ -406,15 +416,17 @@ Bitte geben Sie ein gültiges Alter zwischen 18 und 120 ein - + In welchem Land leben Sie? In welchem Land leben Sie? + Deutschland - + Wie sollen wir Sie kontaktieren? + Nur wenn „Wie lautet Ihre E-Mail-Adresse?“ ist beantwortet Wie sollen wir Sie kontaktieren? @@ -422,15 +434,17 @@ ${q_mandatory} != '' - - Freiwillig, aber willkommen + Haben Sie noch Anmerkungen? + Freiwillig, aber willkommen + Nur wenn „Wie lautet Ihre E-Mail-Adresse?“ ist beantwortet Haben Sie noch Anmerkungen? ${q_mandatory} != '' yes + Keine Anmerkungen diff --git a/tests/fixtures/surveys/all_types_survey/tsv.tsv b/tests/fixtures/surveys/all_types_survey/tsv.tsv index 26cce7e..6615755 100644 --- a/tests/fixtures/surveys/all_types_survey/tsv.tsv +++ b/tests/fixtures/surveys/all_types_survey/tsv.tsv @@ -1,61 +1,61 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip date_format em_validation_q_tip -S language 1 de de -S format 1 A de -S shownoanswer 1 N de -SL surveyls_title 1 Workshop-Feedback-Umfrage de -G 1 G0 1 G0 de -Q S orphanbefore 1 Wie lautet Ihre Teilnehmer-ID? de 1 -G 2 Persönliche Angaben 1 de -Q S qtext 1 Wie heißen Sie (Vor- und Nachname)? de 1 -Q S qstring 1 Wie lautet Ihre E-Mail-Adresse? de 1 -Q N qinteger 1 An wie vielen Workshops haben Sie bisher teilgenommen? de 1 -Q N qint 1 An wie vielen Sessions haben Sie heute teilgenommen? de 1 -Q N qdecimal 1 Wie bewerten Sie den Workshop auf einer Skala von 1,0 bis 10,0? de 1 -Q D qdate 1 Wann sind Sie geboren? de 1 -Q D qtime 1 Um wie viel Uhr sind Sie angekommen? de 1 HH:MM -Q L qselectone 1 Würden Sie diesen Workshop weiterempfehlen? de 1 -A yes Ja de -A no Nein de -Q M qselectmulti 1 An welchen Tracks haben Sie teilgenommen? de 1 -SQ red Frontend-Entwicklung de -SQ blue Backend-Entwicklung de -SQ green Data Science de -Q X qnote 1 Im nächsten Abschnitt bitten wir Sie um Ihr ausführliches Feedback. Bitte nehmen Sie sich Zeit. de -G 3 Ausführliches Feedback 1 de -Q T qmultilinetext 1 Beschreiben Sie bitte Ihre Erfahrungen beim Workshop de 1 -Q T qmultilinestring 1 Welche Verbesserungsvorschläge haben Sie für künftige Veranstaltungen? de 1 -Q L qlikert 1 Wie zufrieden waren Sie mit der Organisation insgesamt? de 1 -A low Schlecht de -A mid Mittelmäßig de -A high Ausgezeichnet de -Q ! qminimal 1 Bevorzugst du Dropdown? de 1 -A yes Ja de -A no Nein de -G 4 Programmierkenntnisse 1 de -Q X matrixwrapper 1 Selbsteinschätzung technischer Fähigkeiten de -Q F matrixheader 1 Wie schätzen Sie Ihre Kenntnisse ein? de 1 -SQ skillpython 1 Python de -SQ skilljs 1 JavaScript de -SQ skillsql 1 SQL de -A none Keine de -A basic Grundkenntnisse de -A adv Fortgeschritten de -A exp Experte de -G 5 Weitere Fragen 1 de -Q L qsel1other 1 Welcher Track hat Ihnen am besten gefallen? de Y 1 -A red Frontend-Entwicklung de -A blue Backend-Entwicklung de -A green Data Science de -Q M qselmother 1 Welche Tracks würden Sie erneut besuchen? de Y 1 -SQ red Frontend-Entwicklung de -SQ blue Backend-Entwicklung de -SQ green Data Science de -G 6 Anmeldedaten 1 de -Q S qmandatory 1 Wie lautet Ihre E-Mail-Adresse? de Y 1 -Q S qhint 1 Wie lautet Ihre Telefonnummer? Format: +49 123 456789 de 1 -Q N qconstraint 1 Wie alt sind Sie? de self >= 18 and self <= 120 1 Bitte geben Sie ein gültiges Alter zwischen 18 und 120 ein -Q S qdefault 1 In welchem Land leben Sie? de Deutschland 1 -Q S qrelevant qmandatory != '' Wie sollen wir Sie kontaktieren? de 1 -Q S qallfeatures qmandatory != '' Haben Sie noch Anmerkungen? Freiwillig, aber willkommen de Y Keine Anmerkungen 1 -G 7 G6 1 G6 de -Q X orphanafter 1 Vielen Dank für Ihre Teilnahme an dieser Umfrage! de \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip date_format num_value_int_only em_validation_q_tip +S language 1 de de +S format 1 A de +S shownoanswer 1 N de +SL surveyls_title 1 Workshop-Feedback-Umfrage de +G 1 G0 1 G0 de +Q S orphanbefore 1 Wie lautet Ihre Teilnehmer-ID? de 1 +G 2 Persönliche Angaben 1 de +Q S qtext 1 Wie heißen Sie (Vor- und Nachname)? de 1 +Q S qstring 1 Wie lautet Ihre E-Mail-Adresse? de 1 +Q N qinteger 1 An wie vielen Workshops haben Sie bisher teilgenommen? de 1 1 +Q N qint 1 An wie vielen Sessions haben Sie heute teilgenommen? de 1 1 +Q N qdecimal 1 Wie bewerten Sie den Workshop auf einer Skala von 1,0 bis 10,0? de 1 +Q D qdate 1 Wann sind Sie geboren? de 1 +Q D qtime 1 Um wie viel Uhr sind Sie angekommen? de 1 HH:MM +Q L qselectone 1 Würden Sie diesen Workshop weiterempfehlen? de 1 +A yes Ja de +A no Nein de +Q M qselectmulti 1 An welchen Tracks haben Sie teilgenommen? de 1 +SQ red Frontend-Entwicklung de +SQ blue Backend-Entwicklung de +SQ green Data Science de +Q X qnote 1 Im nächsten Abschnitt bitten wir Sie um Ihr ausführliches Feedback. Bitte nehmen Sie sich Zeit. de +G 3 Ausführliches Feedback 1 de +Q T qmultilinetext 1 Beschreiben Sie bitte Ihre Erfahrungen beim Workshop de 1 +Q T qmultilinestring 1 Welche Verbesserungsvorschläge haben Sie für künftige Veranstaltungen? de 1 +Q L qlikert 1 Wie zufrieden waren Sie mit der Organisation insgesamt? de 1 +A low Schlecht de +A mid Mittelmäßig de +A high Ausgezeichnet de +Q ! qminimal 1 Bevorzugst du Dropdown? de 1 +A yes Ja de +A no Nein de +G 4 Programmierkenntnisse 1 de +Q X matrixwrapper 1 Selbsteinschätzung technischer Fähigkeiten de +Q F matrixheader 1 Wie schätzen Sie Ihre Kenntnisse ein? de 1 +SQ skillpython 1 Python de +SQ skilljs 1 JavaScript de +SQ skillsql 1 SQL de +A none Keine de +A basic Grundkenntnisse de +A adv Fortgeschritten de +A exp Experte de +G 5 Weitere Fragen 1 de +Q L qsel1other 1 Welcher Track hat Ihnen am besten gefallen? de Y 1 +A red Frontend-Entwicklung de +A blue Backend-Entwicklung de +A green Data Science de +Q M qselmother 1 Welche Tracks würden Sie erneut besuchen? de Y 1 +SQ red Frontend-Entwicklung de +SQ blue Backend-Entwicklung de +SQ green Data Science de +G 6 Anmeldedaten 1 de +Q S qmandatory 1 Wie lautet Ihre E-Mail-Adresse? de Y 1 +Q S qhint 1 Wie lautet Ihre Telefonnummer? Format: +49 123 456789 de 1 +Q N qconstraint 1 Wie alt sind Sie? de self >= 18 and self <= 120 1 1 Bitte geben Sie ein gültiges Alter zwischen 18 und 120 ein +Q S qdefault 1 In welchem Land leben Sie? de Deutschland 1 +Q S qrelevant qmandatory != '' Wie sollen wir Sie kontaktieren? de 1 +Q S qallfeatures qmandatory != '' Haben Sie noch Anmerkungen? Freiwillig, aber willkommen de Y Keine Anmerkungen 1 +G 7 G6 1 G6 de +Q X orphanafter 1 Vielen Dank für Ihre Teilnahme an dieser Umfrage! de \ No newline at end of file diff --git a/tests/fixtures/surveys/appearances_survey/ddi.xml b/tests/fixtures/surveys/appearances_survey/ddi.xml index 9a64a4e..0a6a13b 100644 --- a/tests/fixtures/surveys/appearances_survey/ddi.xml +++ b/tests/fixtures/surveys/appearances_survey/ddi.xml @@ -24,17 +24,19 @@ Über Sie Über Sie + field-list Wie oft nutzen Sie … Wie oft nutzen Sie … + field-list Wie wichtig ist Ihnen … Wie wichtig ist Ihnen … - + Wo wohnen Sie? @@ -47,16 +49,18 @@ Wo wohnen Sie? + minimal - + Möchten Sie etwas ergänzen? Möchten Sie etwas ergänzen? + multiline - + Wie zufrieden sind Sie? @@ -73,9 +77,10 @@ Wie zufrieden sind Sie? + likert - + Bus @@ -88,9 +93,10 @@ Bus + list-nolabel - + Bahn @@ -103,9 +109,10 @@ Bahn + list-nolabel - + Wie wichtig ist Ihnen … … der Preis? @@ -121,7 +128,7 @@ - + Wie wichtig ist Ihnen … … der Takt? @@ -137,14 +144,14 @@ - + Herkunftsland? Herkunftsland? - + Wohin sind Sie gereist? Wohin sind Sie gereist? diff --git a/tests/fixtures/surveys/basic_survey/ddi.xml b/tests/fixtures/surveys/basic_survey/ddi.xml index ed2d449..e9b20bc 100644 --- a/tests/fixtures/surveys/basic_survey/ddi.xml +++ b/tests/fixtures/surveys/basic_survey/ddi.xml @@ -24,15 +24,15 @@ - + What is your name? What is your name? yes - - + + What is your age? What is your age? @@ -40,7 +40,7 @@ yes - + Do you consent to participate? diff --git a/tests/fixtures/surveys/basic_survey/tsv.tsv b/tests/fixtures/surveys/basic_survey/tsv.tsv index 46707d4..8eb13ec 100644 --- a/tests/fixtures/surveys/basic_survey/tsv.tsv +++ b/tests/fixtures/surveys/basic_survey/tsv.tsv @@ -1,12 +1,12 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 en en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Basic Survey en -G Questions 1 en -Q S respondentname 1 What is your name? en Y 1 -Q N age 1 What is your age? en Y 1 -Q L consent 1 Do you consent to participate? en Y 1 -A yes Yes en -A no No en -Q X thankyou consent == 'yes' Thank you for your participation! en \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 en en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Basic Survey en +G Questions 1 en +Q S respondentname 1 What is your name? en Y 1 +Q N age 1 What is your age? en Y 1 1 +Q L consent 1 Do you consent to participate? en Y 1 +A yes Yes en +A no No en +Q X thankyou consent == 'yes' Thank you for your participation! en \ No newline at end of file diff --git a/tests/fixtures/surveys/bilingual_survey/ddi.xml b/tests/fixtures/surveys/bilingual_survey/ddi.xml index f836dea..62663a5 100644 --- a/tests/fixtures/surveys/bilingual_survey/ddi.xml +++ b/tests/fixtures/surveys/bilingual_survey/ddi.xml @@ -45,22 +45,20 @@ Wie haben Sie von uns erfahren? - - Alle Angaben sind freiwillig. - -Aktuelle Tätigkeit - All answers are optional. - -Current job + + Alle Angaben sind freiwillig. + All answers are optional. Was ist Ihr Beruf? What is your occupation? + Aktuelle Tätigkeit + Current job Nicht vorlesen Was ist Ihr Beruf? - + Höchster Abschluss? Highest degree? @@ -82,7 +80,7 @@ Current job - + Welche Sprachen sprechen Sie? Which languages do you speak? Deutsch @@ -98,7 +96,7 @@ Current job - + Welche Sprachen sprechen Sie? Which languages do you speak? Englisch @@ -114,7 +112,7 @@ Current job - + Wie haben Sie von uns erfahren? How did you hear about us? @@ -137,9 +135,10 @@ Current job - + Sonstiges: Other: + Nur wenn „Wie haben Sie von uns erfahren?“ = Sonstiges: Only if “How did you hear about us?” = Other: @@ -148,7 +147,7 @@ Current job ${quelle} = 'other' - + Wie sehr vertrauen Sie … How much do you trust … … dem Parlament? @@ -175,7 +174,7 @@ Current job - + Wie sehr vertrauen Sie … How much do you trust … … der Polizei? diff --git a/tests/fixtures/surveys/complex_survey/ddi.xml b/tests/fixtures/surveys/complex_survey/ddi.xml index 84c3649..ed4e7fc 100644 --- a/tests/fixtures/surveys/complex_survey/ddi.xml +++ b/tests/fixtures/surveys/complex_survey/ddi.xml @@ -38,15 +38,15 @@ Only if “Age” ≥ 18 - + Full Name Full Name yes - - + + Age @@ -58,7 +58,7 @@ Age must be between 18 and 120 - + Gender @@ -77,9 +77,10 @@ - + What are your favorite colors? Red + Select all that apply Only if “Age” ≥ 18 @@ -92,9 +93,10 @@ - + What are your favorite colors? Blue + Select all that apply Only if “Age” ≥ 18 @@ -107,9 +109,10 @@ - + What are your favorite colors? Green + Select all that apply Only if “Age” ≥ 18 @@ -122,9 +125,10 @@ - + What are your favorite colors? Yellow + Select all that apply Only if “Age” ≥ 18 @@ -137,7 +141,7 @@ - + How satisfied are you? Only if “Age” ≥ 18 @@ -153,8 +157,9 @@ - + Any other comments? + Only if “Age” ≥ 18 and “How satisfied are you?” = “unhappy” Any other comments? @@ -162,11 +167,12 @@ ${satisfaction_level} = 'unhappy' - + Date of survey Date of survey - + + today() diff --git a/tests/fixtures/surveys/complex_survey/tsv.tsv b/tests/fixtures/surveys/complex_survey/tsv.tsv index b797609..8c5d0dc 100644 --- a/tests/fixtures/surveys/complex_survey/tsv.tsv +++ b/tests/fixtures/surveys/complex_survey/tsv.tsv @@ -1,24 +1,24 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip em_validation_q_tip -S language 1 en en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Complex Survey with Groups and Logic en -G 1 Demographics 1 en -Q S fullname 1 Full Name en Y 1 -Q N age 1 Age en self >= 18 and self <= 120 1 Age must be between 18 and 120 -Q L gender 1 Gender en 1 -A male Male en -A feml Woman en -A othr Other en -G 2 Preferences age >= 18 en -Q M favoritecolors 1 What are your favorite colors? Select all that apply en 1 -SQ red Red en -SQ blue Blue en -SQ green Green en -SQ yello Yellow en -Q L satisfactionlevel 1 How satisfied are you? en 1 -A happ Happy en -A unhp Unhappy en -Q S othercomments satisfactionlevel == 'unhappy' Any other comments? en 1 -G 3 G2 1 G2 en -Q D surveydate 1 Date of survey en today() 1 \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only em_validation_q_tip +S language 1 en en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Complex Survey with Groups and Logic en +G 1 Demographics 1 en +Q S fullname 1 Full Name en Y 1 +Q N age 1 Age en self >= 18 and self <= 120 1 1 Age must be between 18 and 120 +Q L gender 1 Gender en 1 +A male Male en +A feml Woman en +A othr Other en +G 2 Preferences age >= 18 en +Q M favoritecolors 1 What are your favorite colors? Select all that apply en 1 +SQ red Red en +SQ blue Blue en +SQ green Green en +SQ yello Yellow en +Q L satisfactionlevel 1 How satisfied are you? en 1 +A happ Happy en +A unhp Unhappy en +Q S othercomments satisfactionlevel == 'unhappy' Any other comments? en 1 +G 3 G2 1 G2 en +Q D surveydate 1 Date of survey en today() 1 \ No newline at end of file diff --git a/tests/fixtures/surveys/complex_xpath_survey/ddi.xml b/tests/fixtures/surveys/complex_xpath_survey/ddi.xml index c9bac4c..044b916 100644 --- a/tests/fixtures/surveys/complex_xpath_survey/ddi.xml +++ b/tests/fixtures/surveys/complex_xpath_survey/ddi.xml @@ -27,29 +27,30 @@ Consent and Demographics - + Do you consent to participate? Do you consent to participate? - - + + What is your age? What is your age? - + What country do you live in? What country do you live in? - + Adult consent verification + Only if “Do you consent to participate?” = “yes” and “What is your age?” ≥ 18 Adult consent verification @@ -57,8 +58,9 @@ selected(${consent}, 'yes') and ${age} >= 18 - + Eligible participant + Only if “What is your age?” ≥ 18 and (“What country do you live in?” = “USA” or “What country do you live in?” = “Canada”) Eligible participant @@ -66,8 +68,9 @@ ${age} >= 18 and (${country} = 'USA' or ${country} = 'Canada') - + Valid response check + Only if not (“Do you consent to participate?” is not answered) and “What is your age?” > 0 Valid response check @@ -75,16 +78,18 @@ not(${consent} = '') and ${age} > 0 - + Age category + Age category if(${age} > 18, true(), false()) - + Complex validation test + Only if “Do you consent to participate?” = “yes” and (“What is your age?” ≥ 18 or “What country do you live in?” = “USA”) Complex validation test diff --git a/tests/fixtures/surveys/complex_xpath_survey/tsv.tsv b/tests/fixtures/surveys/complex_xpath_survey/tsv.tsv index 4fb774c..3b841ed 100644 --- a/tests/fixtures/surveys/complex_xpath_survey/tsv.tsv +++ b/tests/fixtures/surveys/complex_xpath_survey/tsv.tsv @@ -1,14 +1,14 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 en en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Complex XPath Survey en -G 1 Consent and Demographics 1 en -Q S consent 1 Do you consent to participate? en 1 -Q N age 1 What is your age? en 1 -Q S country 1 What country do you live in? en 1 -Q S adultconsent (consent.NAOK=='yes') and age >= 18 Adult consent verification en 1 -Q S eligibleparticipant age >= 18 and (country == 'USA' or country == 'Canada') Eligible participant en 1 -Q S validresponse !(consent == '') and age > 0 Valid response check en 1 -Q S agecategory if(age > 18, 1, 0) Age category en 1 -Q S complexvalidation consent == 'yes' and (age >= 18 or country == 'USA') Complex validation test en self >= 5 and self <= 100 1 \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 en en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Complex XPath Survey en +G 1 Consent and Demographics 1 en +Q S consent 1 Do you consent to participate? en 1 +Q N age 1 What is your age? en 1 1 +Q S country 1 What country do you live in? en 1 +Q S adultconsent (consent.NAOK=='yes') and age >= 18 Adult consent verification en 1 +Q S eligibleparticipant age >= 18 and (country == 'USA' or country == 'Canada') Eligible participant en 1 +Q S validresponse !(consent == '') and age > 0 Valid response check en 1 +Q S agecategory if(age > 18, 1, 0) Age category en 1 +Q S complexvalidation consent == 'yes' and (age >= 18 or country == 'USA') Complex validation test en self >= 5 and self <= 100 1 \ No newline at end of file diff --git a/tests/fixtures/surveys/grid_survey/ddi.xml b/tests/fixtures/surveys/grid_survey/ddi.xml index e3335ac..f878e61 100644 --- a/tests/fixtures/surveys/grid_survey/ddi.xml +++ b/tests/fixtures/surveys/grid_survey/ddi.xml @@ -32,9 +32,10 @@ Nun zu Ihrer Mediennutzung. - + Wie sehr vertrauen Sie … … dem Parlament? + Bundestag Skala vorlesen @@ -53,7 +54,7 @@ - + Wie sehr vertrauen Sie … … der Polizei? @@ -73,9 +74,10 @@ - + Welche Medien nutzen Sie? Fernsehen + Mehrfachnennung möglich 0 @@ -87,9 +89,10 @@ - + Welche Medien nutzen Sie? Radio + Mehrfachnennung möglich 0 @@ -100,12 +103,11 @@ Welche Medien nutzen Sie?: Radio - - - Zuletzt eine Angabe zur Person. - -In Jahren + + + Zuletzt eine Angabe zur Person. Wie alt sind Sie? + In Jahren Wie alt sind Sie? diff --git a/tests/fixtures/surveys/grid_survey/tsv.tsv b/tests/fixtures/surveys/grid_survey/tsv.tsv index 29c51ee..90f2841 100644 --- a/tests/fixtures/surveys/grid_survey/tsv.tsv +++ b/tests/fixtures/surveys/grid_survey/tsv.tsv @@ -1,21 +1,21 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 de de -S format 1 A de -S shownoanswer 1 N de -SL surveyls_title 1 Grid-Hinweise de -G 1 G0 1 G0 de -Q X intro 1 Einige Fragen zu Ihrem Vertrauen. de -G 2 Wie sehr vertrauen Sie … 1 de -Q F vertrauen 1 Wie sehr vertrauen Sie … de 1 -SQ parlament 1 … dem Parlament? de -SQ polizei 1 … der Polizei? de -A 1 Gar nicht de -A 2 Etwas de -A 3 Voll de -G 4 G3 1 G3 de -Q X medienhinweis 1 Nun zu Ihrer Mediennutzung. de -Q M medien 1 Welche Medien nutzen Sie? Mehrfachnennung möglich de 1 -SQ tv Fernsehen de -SQ radio Radio de -Q X alterhinweis 1 Zuletzt eine Angabe zur Person. de -Q N alter 1 Wie alt sind Sie? In Jahren de 1 \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 de de +S format 1 A de +S shownoanswer 1 N de +SL surveyls_title 1 Grid-Hinweise de +G 1 G0 1 G0 de +Q X intro 1 Einige Fragen zu Ihrem Vertrauen. de +G 2 Wie sehr vertrauen Sie … 1 de +Q F vertrauen 1 Wie sehr vertrauen Sie … de 1 +SQ parlament 1 … dem Parlament? de +SQ polizei 1 … der Polizei? de +A 1 Gar nicht de +A 2 Etwas de +A 3 Voll de +G 4 G3 1 G3 de +Q X medienhinweis 1 Nun zu Ihrer Mediennutzung. de +Q M medien 1 Welche Medien nutzen Sie? Mehrfachnennung möglich de 1 +SQ tv Fernsehen de +SQ radio Radio de +Q X alterhinweis 1 Zuletzt eine Angabe zur Person. de +Q N alter 1 Wie alt sind Sie? In Jahren de 1 1 \ No newline at end of file diff --git a/tests/fixtures/surveys/hints_survey/ddi.xml b/tests/fixtures/surveys/hints_survey/ddi.xml index c52b661..d6cff78 100644 --- a/tests/fixtures/surveys/hints_survey/ddi.xml +++ b/tests/fixtures/surveys/hints_survey/ddi.xml @@ -26,27 +26,26 @@ Welche Medien nutzen Sie? Welche Medien nutzen Sie? - - - Die folgenden Fragen betreffen Ihren Haushalt. - -Sie selbst mitgezählt. + + + Die folgenden Fragen betreffen Ihren Haushalt. Wie viele Personen leben in Ihrem Haushalt? + Sie selbst mitgezählt. Wie viele Personen leben in Ihrem Haushalt? - - Bitte möglichst genau. + Was ist Ihr Beruf? + Bitte möglichst genau. Bei Unklarheit nach der Tätigkeit fragen. Was ist Ihr Beruf? - + Sind Sie ehrenamtlich tätig? Auch gelegentliches Engagement zählt. @@ -62,9 +61,10 @@ Sie selbst mitgezählt. - + Welche Medien nutzen Sie? Fernsehen + Mehrfachnennung möglich. 0 @@ -76,9 +76,10 @@ Sie selbst mitgezählt. - + Welche Medien nutzen Sie? Radio + Mehrfachnennung möglich. 0 diff --git a/tests/fixtures/surveys/hints_survey/tsv.tsv b/tests/fixtures/surveys/hints_survey/tsv.tsv index 5f4e03e..58e6384 100644 --- a/tests/fixtures/surveys/hints_survey/tsv.tsv +++ b/tests/fixtures/surveys/hints_survey/tsv.tsv @@ -1,15 +1,15 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 de de -S format 1 A de -S shownoanswer 1 N de -SL surveyls_title 1 Hinweise de -G Questions 1 de -Q X intro 1 Die folgenden Fragen betreffen Ihren Haushalt. de -Q N personen 1 Wie viele Personen leben in Ihrem Haushalt? Sie selbst mitgezählt. de 1 -Q S beruf 1 Was ist Ihr Beruf? Bitte möglichst genau. de 1 -Q L ehrenamt 1 Sind Sie ehrenamtlich tätig? de 1 -A ja Ja de -A nein Nein de -Q M medien 1 Welche Medien nutzen Sie? Mehrfachnennung möglich. de 1 -SQ tv Fernsehen de -SQ radio Radio de \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 de de +S format 1 A de +S shownoanswer 1 N de +SL surveyls_title 1 Hinweise de +G Questions 1 de +Q X intro 1 Die folgenden Fragen betreffen Ihren Haushalt. de +Q N personen 1 Wie viele Personen leben in Ihrem Haushalt? Sie selbst mitgezählt. de 1 1 +Q S beruf 1 Was ist Ihr Beruf? Bitte möglichst genau. de 1 +Q L ehrenamt 1 Sind Sie ehrenamtlich tätig? de 1 +A ja Ja de +A nein Nein de +Q M medien 1 Welche Medien nutzen Sie? Mehrfachnennung möglich. de 1 +SQ tv Fernsehen de +SQ radio Radio de \ No newline at end of file diff --git a/tests/fixtures/surveys/multilingual_survey/ddi.xml b/tests/fixtures/surveys/multilingual_survey/ddi.xml index 5f928c1..bd1f25b 100644 --- a/tests/fixtures/surveys/multilingual_survey/ddi.xml +++ b/tests/fixtures/surveys/multilingual_survey/ddi.xml @@ -28,17 +28,17 @@ Demographics - - Enter your full name - Ingresa tu nombre completo + What is your name? ¿Cuál es tu nombre? + Enter your full name + Ingresa tu nombre completo What is your name? - - + + What is your age? ¿Cuál es tu edad? @@ -46,7 +46,7 @@ - + Gender Género @@ -69,7 +69,7 @@ - + How satisfied are you? ¿Qué tan satisfecho estás? diff --git a/tests/fixtures/surveys/multilingual_survey/tsv.tsv b/tests/fixtures/surveys/multilingual_survey/tsv.tsv index ac7d20c..a6efc22 100644 --- a/tests/fixtures/surveys/multilingual_survey/tsv.tsv +++ b/tests/fixtures/surveys/multilingual_survey/tsv.tsv @@ -1,31 +1,31 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 en en -S additional_languages 1 es en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Multilingual Survey en -SL surveyls_title 1 Encuesta multilingüe es -G 1 Demographics 1 en -G 1 Datos demográficos 1 es -Q S name 1 What is your name? Enter your full name en 1 -Q N age 1 What is your age? en 1 -Q L gender 1 Gender en 1 -A male Male en -A femal Female en -A other Other en -Q S name 1 ¿Cuál es tu nombre? Ingresa tu nombre completo es 1 -Q N age 1 ¿Cuál es tu edad? es 1 -Q L gender 1 Género es 1 -A male Masculino es -A femal Femenino es -A other Otro es -G 2 G1 1 G1 en -G 2 G1 1 G1 es -Q L satisfaction 1 How satisfied are you? en 1 -A happy Very satisfied en -A neutr Neutral en -A unhap Not satisfied en -Q L satisfaction 1 ¿Qué tan satisfecho estás? es 1 -A happy Muy satisfecho es -A neutr Neutral es -A unhap No satisfecho es \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 en en +S additional_languages 1 es en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Multilingual Survey en +SL surveyls_title 1 Encuesta multilingüe es +G 1 Demographics 1 en +G 1 Datos demográficos 1 es +Q S name 1 What is your name? Enter your full name en 1 +Q N age 1 What is your age? en 1 1 +Q L gender 1 Gender en 1 +A male Male en +A femal Female en +A other Other en +Q S name 1 ¿Cuál es tu nombre? Ingresa tu nombre completo es 1 +Q N age 1 ¿Cuál es tu edad? es 1 1 +Q L gender 1 Género es 1 +A male Masculino es +A femal Femenino es +A other Otro es +G 2 G1 1 G1 en +G 2 G1 1 G1 es +Q L satisfaction 1 How satisfied are you? en 1 +A happy Very satisfied en +A neutr Neutral en +A unhap Not satisfied en +Q L satisfaction 1 ¿Qué tan satisfecho estás? es 1 +A happy Muy satisfecho es +A neutr Neutral es +A unhap No satisfecho es \ No newline at end of file diff --git a/tests/fixtures/surveys/multipage_survey/ddi.xml b/tests/fixtures/surveys/multipage_survey/ddi.xml index 808ec9d..4cd46da 100644 --- a/tests/fixtures/surveys/multipage_survey/ddi.xml +++ b/tests/fixtures/surveys/multipage_survey/ddi.xml @@ -11,6 +11,7 @@ Thank you! + pages @@ -26,27 +27,29 @@ Page One Page One + field-list Page Two Page Two + field-list - + Your name Your name - - + + Your age Your age - + Favourite colour diff --git a/tests/fixtures/surveys/multipage_survey/tsv.tsv b/tests/fixtures/surveys/multipage_survey/tsv.tsv index d7b6e3a..e619d33 100644 --- a/tests/fixtures/surveys/multipage_survey/tsv.tsv +++ b/tests/fixtures/surveys/multipage_survey/tsv.tsv @@ -1,14 +1,14 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip -S language 1 en en -S format 1 G en -S shownoanswer 1 N en -SL surveyls_title 1 Multi-Page Survey en -G 1 Page One 1 en -Q S name 1 Your name en 1 -Q N age 1 Your age en 1 -G 2 Page Two 1 en -Q L favcolor 1 Favourite colour en 1 -A red Red en -A blue Blue en -A green Green en -Q X thankyou 1 Thank you! en \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only +S language 1 en en +S format 1 G en +S shownoanswer 1 N en +SL surveyls_title 1 Multi-Page Survey en +G 1 Page One 1 en +Q S name 1 Your name en 1 +Q N age 1 Your age en 1 1 +G 2 Page Two 1 en +Q L favcolor 1 Favourite colour en 1 +A red Red en +A blue Blue en +A green Green en +Q X thankyou 1 Thank you! en \ No newline at end of file diff --git a/tests/fixtures/surveys/settings_survey/ddi.xml b/tests/fixtures/surveys/settings_survey/ddi.xml index d4b17b5..af26309 100644 --- a/tests/fixtures/surveys/settings_survey/ddi.xml +++ b/tests/fixtures/surveys/settings_survey/ddi.xml @@ -33,7 +33,7 @@ What is your _favorite_ color? - + What is your _favorite_ color? @@ -52,8 +52,9 @@ - + Please specify your color + Only if “What is your _favorite_ color?” = Other Please specify your color @@ -61,7 +62,7 @@ ${fav_color} = 'other' - + Any **comments**? Any **comments**? diff --git a/tests/fixtures/surveys/testA/ddi.xml b/tests/fixtures/surveys/testA/ddi.xml index 0a91463..7638d96 100644 --- a/tests/fixtures/surveys/testA/ddi.xml +++ b/tests/fixtures/surveys/testA/ddi.xml @@ -34,6 +34,7 @@ wirkung wirkung + Bitte gib an, wie sehr du zustimmst (1 = stimme gar nicht zu, 5 = stimme voll zu): erfahrungen @@ -52,7 +53,7 @@ Welche Bereiche nutzt du überhaupt? (Mehrfachauswahl möglich) - + Wie lange kommst du schon zu TestWerk? @@ -80,7 +81,7 @@ yes - + Wie oft kommst du ungefähr? @@ -103,16 +104,16 @@ yes - - - z.B. 10, 50, 100... + + Ca. wie viele Stunden warst du insgesamt bei TestWerk? (grobe Schätzung reicht) + z.B. 10, 50, 100... Ca. wie viele Stunden warst du insgesamt bei TestWerk? (grobe Schätzung reicht) - + In welcher Werkstatt bist du hauptsächlich? @@ -140,7 +141,7 @@ yes - + Welche Bereiche nutzt du überhaupt? (Mehrfachauswahl möglich) Holzwerkstatt @@ -154,7 +155,7 @@ - + Welche Bereiche nutzt du überhaupt? (Mehrfachauswahl möglich) Metallwerkstatt @@ -168,7 +169,7 @@ - + Welche Bereiche nutzt du überhaupt? (Mehrfachauswahl möglich) Textilwerkstatt @@ -182,7 +183,7 @@ - + Welche Bereiche nutzt du überhaupt? (Mehrfachauswahl möglich) Medienwerkstatt/Digital @@ -196,7 +197,7 @@ - + Hattest du vorher schon handwerkliche Erfahrung? @@ -216,7 +217,7 @@ yes - + Ich fühle mich bei TestWerk wohl und akzeptiert. @@ -242,9 +243,10 @@ Ich fühle mich bei TestWerk wohl und akzeptiert. yes + likert - + Ich habe bei TestWerk Freund*innen gefunden. @@ -270,9 +272,10 @@ Ich habe bei TestWerk Freund*innen gefunden. yes + likert - + Ich traue mir zu, eigene Ideen und Projekte umzusetzen. @@ -298,9 +301,10 @@ Ich traue mir zu, eigene Ideen und Projekte umzusetzen. yes + likert - + Wenn bei einem Projekt etwas nicht klappt, finde ich eine Lösung. @@ -326,9 +330,10 @@ Wenn bei einem Projekt etwas nicht klappt, finde ich eine Lösung. yes + likert - + Ich habe bei TestWerk handwerkliche Fähigkeiten gelernt. @@ -354,9 +359,10 @@ Ich habe bei TestWerk handwerkliche Fähigkeiten gelernt. yes + likert - + Jetzt denk mal an deine berufliche Zukunft: Bevor ich zu TestWerk kam, hatte ich eine klare Vorstellung von meiner beruflichen Zukunft. @@ -383,9 +389,10 @@ Bevor ich zu TestWerk kam, hatte ich eine klare Vorstellung von meiner beruflichen Zukunft. yes + likert - + Aktuell habe ich eine klare Vorstellung von meiner beruflichen Zukunft. @@ -411,42 +418,48 @@ Aktuell habe ich eine klare Vorstellung von meiner beruflichen Zukunft. yes + likert - - 0% = gar nicht, 100% = komplett + Ca. Wie viel Prozent dieser Veränderung geht auf TestWerk zurück? + 0% = gar nicht, 100% = komplett + + + + Only if “Aktuell habe ich eine klare Vorstellung von meiner beruflichen Zukunft.” ≠ “Bevor ich zu TestWerk kam, hatte ich eine klare Vorstellung von meiner beruflichen Zukunft.” Ca. Wie viel Prozent dieser Veränderung geht auf TestWerk zurück? ${beruf_post} != ${beruf_pre} + step=5 - + Was hast du bei TestWerk gemacht oder gebaut? (gerne Beispiele) Was hast du bei TestWerk gemacht oder gebaut? (gerne Beispiele) - + Was hat dir bei TestWerk am meisten gebracht? Was hat dir bei TestWerk am meisten gebracht? - + Was könnten wir besser machen? Was könnten wir besser machen? - - 0 = auf keinen Fall, 10 = auf jeden Fall + Würdest du TestWerk Freund*innen empfehlen? + 0 = auf keinen Fall, 10 = auf jeden Fall 0 @@ -495,9 +508,10 @@ Würdest du TestWerk Freund*innen empfehlen? yes + likert - + Wie alt bist du? (optional) @@ -520,7 +534,7 @@ - + Geschlecht (optional) @@ -547,8 +561,9 @@ - + Falls "Anderes": Bitte angeben (optional) + Only if “Geschlecht (optional)” = Anderes Geschlecht Falls "Anderes": Bitte angeben (optional) diff --git a/tests/fixtures/surveys/testA/tsv.tsv b/tests/fixtures/surveys/testA/tsv.tsv index 1e39b2d..bef92d2 100644 --- a/tests/fixtures/surveys/testA/tsv.tsv +++ b/tests/fixtures/surveys/testA/tsv.tsv @@ -17,7 +17,7 @@ A 1 1-2 Mal insgesamt en A 2 Ab und zu (1x im Monat) en A 3 Regelmäßig (mehrmals im Monat) en A 4 Sehr oft (jede Woche oder öfter) en -Q N stundengesamt 1 Ca. wie viele Stunden warst du insgesamt bei TestWerk? (grobe Schätzung reicht) z.B. 10, 50, 100... en 1 +Q N stundengesamt 1 Ca. wie viele Stunden warst du insgesamt bei TestWerk? (grobe Schätzung reicht) z.B. 10, 50, 100... en 1 1 Q L hauptwerkstatt 1 In welcher Werkstatt bist du hauptsächlich? en Y 1 A holz Holzwerkstatt en A metal Metallwerkstatt en diff --git a/tests/fixtures/surveys/testB/ddi.xml b/tests/fixtures/surveys/testB/ddi.xml index c43630e..25ce766 100644 --- a/tests/fixtures/surveys/testB/ddi.xml +++ b/tests/fixtures/surveys/testB/ddi.xml @@ -16,6 +16,7 @@ Hello! Disclaimer Disclaimer + theme-grid no-text-transform @@ -42,11 +43,13 @@ Techniken Techniques Techniken + field-list Themen Topics Themen + field-list Du und das Projekt @@ -92,7 +95,7 @@ yes - + Für welche Projekte möchtest Du dich bewerben? Which project(s) do you want to apply for? Projekt Alpha @@ -108,7 +111,7 @@ - + Für welche Projekte möchtest Du dich bewerben? Which project(s) do you want to apply for? Projekt Beta @@ -124,7 +127,7 @@ - + Für welche Projekte möchtest Du dich bewerben? Which project(s) do you want to apply for? Projekt Gamma @@ -140,11 +143,12 @@ - + In welcher Rolle siehst du dich im Projekt Projekt Alpha? Which role do you think you could fill in Project Alpha? Umfragedesign und Datenerhebung Survey design and data collection + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Alpha Only if “Which project(s) do you want to apply for?” includes Project Alpha @@ -158,11 +162,12 @@ - + In welcher Rolle siehst du dich im Projekt Projekt Alpha? Which role do you think you could fill in Project Alpha? Datenanalyse Data analysis + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Alpha Only if “Which project(s) do you want to apply for?” includes Project Alpha @@ -176,11 +181,12 @@ - + In welcher Rolle siehst du dich im Projekt Projekt Alpha? Which role do you think you could fill in Project Alpha? Teamkoordinator:in Team Coordinator + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Alpha Only if “Which project(s) do you want to apply for?” includes Project Alpha @@ -194,11 +200,12 @@ - + In welcher Rolle siehst du dich im Projekt Projekt Alpha? Which role do you think you could fill in Project Alpha? Team Trainee Team Trainee + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Alpha Only if “Which project(s) do you want to apply for?” includes Project Alpha @@ -212,11 +219,12 @@ - + In welcher Rolle siehst du dich im Projekt Beta? Which role do you see yourself in for Project Beta? Datenvisualisierung Data visualization + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Beta Only if “Which project(s) do you want to apply for?” includes Project Beta @@ -230,11 +238,12 @@ - + In welcher Rolle siehst du dich im Projekt Beta? Which role do you see yourself in for Project Beta? Data Engineering Data engineering + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Beta Only if “Which project(s) do you want to apply for?” includes Project Beta @@ -248,11 +257,12 @@ - + In welcher Rolle siehst du dich im Projekt Beta? Which role do you see yourself in for Project Beta? Teamkoordinator:in Team Coordinator + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Beta Only if “Which project(s) do you want to apply for?” includes Project Beta @@ -266,11 +276,12 @@ - + In welcher Rolle siehst du dich im Projekt Beta? Which role do you see yourself in for Project Beta? Team Trainee Team Trainee + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Beta Only if “Which project(s) do you want to apply for?” includes Project Beta @@ -284,11 +295,12 @@ - + In welcher Rolle siehst du dich im Projekt Gamma? Which role do you see yourself in for Project Gamma? Projektmanagement Project management + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Gamma Only if “Which project(s) do you want to apply for?” includes Project Gamma @@ -302,11 +314,12 @@ - + In welcher Rolle siehst du dich im Projekt Gamma? Which role do you see yourself in for Project Gamma? Machine Learning Machine learning + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Gamma Only if “Which project(s) do you want to apply for?” includes Project Gamma @@ -320,11 +333,12 @@ - + In welcher Rolle siehst du dich im Projekt Gamma? Which role do you see yourself in for Project Gamma? Teamkoordinator:in Team Coordinator + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Gamma Only if “Which project(s) do you want to apply for?” includes Project Gamma @@ -338,11 +352,12 @@ - + In welcher Rolle siehst du dich im Projekt Gamma? Which role do you see yourself in for Project Gamma? Team Trainee Team Trainee + Nur wenn „Für welche Projekte möchtest Du dich bewerben?“ enthält Projekt Gamma Only if “Which project(s) do you want to apply for?” includes Project Gamma @@ -356,9 +371,10 @@ - + SoSci Survey SoSci Survey + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -386,11 +402,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Python Python + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Datenanalyse Only if “Which role do you think you could fill in Project Alpha?” includes Data analysis @@ -418,11 +436,13 @@ selected(${project_role_project-alpha}, 'role-data-analysis') yes + list-nolabel - + R R + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Datenanalyse Only if “Which role do you think you could fill in Project Alpha?” includes Data analysis @@ -450,11 +470,13 @@ selected(${project_role_project-alpha}, 'role-data-analysis') yes + list-nolabel - + Power BI Power BI + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Datenvisualisierung Only if “Which role do you see yourself in for Project Beta?” includes Data visualization @@ -482,11 +504,13 @@ selected(${project_role_project-beta}, 'role-visualization') yes + list-nolabel - + Microsoft Excel Microsoft Excel + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Datenvisualisierung Only if “Which role do you see yourself in for Project Beta?” includes Data visualization @@ -514,11 +538,13 @@ selected(${project_role_project-beta}, 'role-visualization') yes + list-nolabel - + SQL SQL + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Data Engineering Only if “Which role do you see yourself in for Project Beta?” includes Data engineering @@ -546,11 +572,13 @@ selected(${project_role_project-beta}, 'role-data-engineering') yes + list-nolabel - + Git Git + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Data Engineering Only if “Which role do you see yourself in for Project Beta?” includes Data engineering @@ -578,11 +606,13 @@ selected(${project_role_project-beta}, 'role-data-engineering') yes + list-nolabel - + Jupyter Notebooks Jupyter Notebooks + Nur wenn „In welcher Rolle siehst du dich im Projekt Gamma?“ enthält Machine Learning Only if “Which role do you see yourself in for Project Gamma?” includes Machine learning @@ -610,11 +640,13 @@ selected(${project_role_project-gamma}, 'role-machine-learning') yes + list-nolabel - + TensorFlow / PyTorch TensorFlow / PyTorch + Nur wenn „In welcher Rolle siehst du dich im Projekt Gamma?“ enthält Machine Learning Only if “Which role do you see yourself in for Project Gamma?” includes Machine learning @@ -642,21 +674,24 @@ selected(${project_role_project-gamma}, 'role-machine-learning') yes + list-nolabel - - Bitte gib hier andere Tools an, die du kennst und die du für relevant erachtest. - Please specifiy other tools you know that you consider relevant + Andere relevante Tools Other relevant tools + Bitte gib hier andere Tools an, die du kennst und die du für relevant erachtest. + Please specifiy other tools you know that you consider relevant Andere relevante Tools + multiline - + Entwicklung von Fragebögen Survey design + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -684,11 +719,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Entwicklung von Indikatoren Development of indicators + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -716,11 +753,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Datenerhebung Data collection + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -748,11 +787,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Datenbereinigung Data cleaning + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Datenanalyse Only if “Which role do you think you could fill in Project Alpha?” includes Data analysis @@ -780,11 +821,13 @@ selected(${project_role_project-alpha}, 'role-data-analysis') yes + list-nolabel - + Deskriptive Statistik Descriptive statistics + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Datenanalyse Only if “Which role do you think you could fill in Project Alpha?” includes Data analysis @@ -812,11 +855,13 @@ selected(${project_role_project-alpha}, 'role-data-analysis') yes + list-nolabel - + Datenvisualisierung Data visualization + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Datenvisualisierung Only if “Which role do you see yourself in for Project Beta?” includes Data visualization @@ -844,11 +889,13 @@ selected(${project_role_project-beta}, 'role-visualization') yes + list-nolabel - + Data Engineering Data engineering + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Data Engineering Only if “Which role do you see yourself in for Project Beta?” includes Data engineering @@ -876,11 +923,13 @@ selected(${project_role_project-beta}, 'role-data-engineering') yes + list-nolabel - + Automatisierung Automation + Nur wenn „In welcher Rolle siehst du dich im Projekt Beta?“ enthält Data Engineering Only if “Which role do you see yourself in for Project Beta?” includes Data engineering @@ -908,11 +957,13 @@ selected(${project_role_project-beta}, 'role-data-engineering') yes + list-nolabel - + Projektplanung Project planning + Nur wenn „In welcher Rolle siehst du dich im Projekt Gamma?“ enthält Projektmanagement Only if “Which role do you see yourself in for Project Gamma?” includes Project management @@ -940,11 +991,13 @@ selected(${project_role_project-gamma}, 'role-project-management') yes + list-nolabel - + ML-Modellierung ML modeling + Nur wenn „In welcher Rolle siehst du dich im Projekt Gamma?“ enthält Machine Learning Only if “Which role do you see yourself in for Project Gamma?” includes Machine learning @@ -972,11 +1025,13 @@ selected(${project_role_project-gamma}, 'role-machine-learning') yes + list-nolabel - + Wirkungsmessung Impact Measurement + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1004,11 +1059,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Research Design Research Design + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1036,11 +1093,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Umfrageforschung Survey Research + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1068,11 +1127,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Datenschutz Data protection + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1100,11 +1161,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Resilienz und Mentale Gesundheit Resilience and mental health + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1132,11 +1195,13 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - + Bildungsforschung o.ä. Education research (or similar) + Nur wenn „In welcher Rolle siehst du dich im Projekt Projekt Alpha?“ enthält Umfragedesign und Datenerhebung Only if “Which role do you think you could fill in Project Alpha?” includes Survey design and data collection @@ -1164,35 +1229,38 @@ selected(${project_role_project-alpha}, 'role-survey-design') yes + list-nolabel - - Bitte schreibe maximal 5 Sätze pro Projekt, auf das du dich bewerben möchtest. - Please write maximum 5 sentences per project that you are applying to. + Bitte beschreibe hier, welche Deiner Fähigkeiten und Erfahrungen Dich besonders für die Teilnahme an diesem Projekt qualifizieren. Please describe here what skills and experiences you would bring to the project. + Bitte schreibe maximal 5 Sätze pro Projekt, auf das du dich bewerben möchtest. + Please write maximum 5 sentences per project that you are applying to. Bitte beschreibe hier, welche Deiner Fähigkeiten und Erfahrungen Dich besonders für die Teilnahme an diesem Projekt qualifizieren. yes + multiline - - Bitte schreibe maximal 5 Sätze pro Projekt, auf das du dich bewerben möchtest. - Please write maximum 5 sentences per project that you are applying to. + Bitte beschreibe hier, warum Du Dich für dieses Projekt engagieren möchtest. Please describe here why you want to get involved in this project. + Bitte schreibe maximal 5 Sätze pro Projekt, auf das du dich bewerben möchtest. + Please write maximum 5 sentences per project that you are applying to. Bitte beschreibe hier, warum Du Dich für dieses Projekt engagieren möchtest. yes + multiline - - Um möglichst vielen Personen die Gelegenheit zu geben, an unseren Projekten teilzunehmen, werden Bewerbungen von bisher unerfolgreichen Personen bei gleicher Eignung bevorzugt behandelt. - In order to give as many people as possible the opportunity to participate in one of our projects, applications of individuals who have unsuccessfully applied in the past are preferred given equal suitability for the project. + Hast du dich in der Vergangenheit bereits auf CorrelAid-Projekte beworben? Have you applied to a CorrelAid project in the past? + Um möglichst vielen Personen die Gelegenheit zu geben, an unseren Projekten teilzunehmen, werden Bewerbungen von bisher unerfolgreichen Personen bei gleicher Eignung bevorzugt behandelt. + In order to give as many people as possible the opportunity to participate in one of our projects, applications of individuals who have unsuccessfully applied in the past are preferred given equal suitability for the project. successful @@ -1214,11 +1282,12 @@ yes - - Grobe Angaben helfen uns auch schon weiter. - Any information is helpful to us. + Bitte gib hier an, für welche Projekte du dich beworben hast, falls du dich noch erinnern kannst. If you can remember, please specify which project(s) you have applied for. + Grobe Angaben helfen uns auch schon weiter. + Any information is helpful to us. + Nur wenn „Hast du dich in der Vergangenheit bereits auf CorrelAid-Projekte beworben?“ = Ja, aber ich war nicht erfolgreich / ich wurde bis jetzt immer abgelehnt. Only if “Have you applied to a CorrelAid project in the past?” = Yes, but I have not been successful / all my applications have been rejected so far. @@ -1227,7 +1296,7 @@ ${past_applications} = 'not_successful' - + Wie lautet dein Vorname? What is your first name? @@ -1236,7 +1305,7 @@ yes - + Wie lautet dein Nachname? What is your last name? @@ -1245,7 +1314,7 @@ yes - + Unter welcher E-Mail Adresse können wir dich erreichen? Which email address can we use to contact you? @@ -1256,11 +1325,11 @@ yes - - Hinweis: Da wir bei CorrelAid nach dem Grundprinzip von Geschlechtergleichberechtigung arbeiten, ist diese Frage für uns besonders wichtig. - Note: We ask this question because we want to foster gender diversity within CorrelAid. + Was ist dein Geschlecht? What is your gender? + Hinweis: Da wir bei CorrelAid nach dem Grundprinzip von Geschlechtergleichberechtigung arbeiten, ist diese Frage für uns besonders wichtig. + Note: We ask this question because we want to foster gender diversity within CorrelAid. female @@ -1292,11 +1361,12 @@ yes - - Bitte gebe hier dein Geschlecht an. - Please specify your gender here. + Mein Geschlecht ist: My gender is: + Bitte gebe hier dein Geschlecht an. + Please specify your gender here. + Nur wenn „Was ist dein Geschlecht?“ = Mein Geschlecht ist (eigene Angabe): Only if “What is your gender?” = My gender is (manually specify): @@ -1306,11 +1376,11 @@ yes - - Ich habe den Disclaimer gelesen und erteile hiermit meine Zustimmung zur Erhebung, Verarbeitung und Nutzung meiner personenbezogener Daten (Geschlecht und persönliche Fähigkeiten) zu internen und externen Reporting- und Präsentationszwecken. Daneben erteile ich ausdrücklich die Erlaubnis meine Kontaktdaten (Name und E-Mail) zur Kontaktaufnahme zu nutzen und diese bei erfolgreicher Bewerbung mit dem Projektteam zu teilen. - I have read the disclaimer and hereby give my consent to the collection, processing and use of my personal data (gender and personal skills) for the team selection process and for internal and external reporting as well as presentation purposes. In addition, I expressly grant permission to use my contact details (name and e-mail) to contact me and to share them with the project team if the application is successful. + Einwilligung in die Datenschutzerklärung Consent to privacy policy + Ich habe den Disclaimer gelesen und erteile hiermit meine Zustimmung zur Erhebung, Verarbeitung und Nutzung meiner personenbezogener Daten (Geschlecht und persönliche Fähigkeiten) zu internen und externen Reporting- und Präsentationszwecken. Daneben erteile ich ausdrücklich die Erlaubnis meine Kontaktdaten (Name und E-Mail) zur Kontaktaufnahme zu nutzen und diese bei erfolgreicher Bewerbung mit dem Projektteam zu teilen. + I have read the disclaimer and hereby give my consent to the collection, processing and use of my personal data (gender and personal skills) for the team selection process and for internal and external reporting as well as presentation purposes. In addition, I expressly grant permission to use my contact details (name and e-mail) to contact me and to share them with the project team if the application is successful. yes diff --git a/tests/fixtures/surveys/validation_relevance_survey/ddi.xml b/tests/fixtures/surveys/validation_relevance_survey/ddi.xml index 893d001..6285f3b 100644 --- a/tests/fixtures/surveys/validation_relevance_survey/ddi.xml +++ b/tests/fixtures/surveys/validation_relevance_survey/ddi.xml @@ -22,7 +22,7 @@ - + Validation and Relevance Test Survey Username @@ -31,8 +31,8 @@ . != '' Username cannot be empty - - + + Age @@ -44,7 +44,7 @@ Age must be between 18 and 120 - + Price @@ -56,7 +56,7 @@ Price must be between 0 and 1000 - + Do you consent? @@ -71,8 +71,9 @@ - + Adult Information + Only if “Age” ≥ 18 Adult Information @@ -81,8 +82,9 @@ . != '' - + Senior Discount Code + Only if “Age” ≥ 65 Senior Discount Code @@ -90,8 +92,9 @@ ${age} >= 65 - + Additional Information + Only if “Do you consent?” = Yes Additional Information @@ -99,8 +102,9 @@ ${consent} = 'yes' - + Complex Relevance Test + Only if “Age” ≥ 18 and “Do you consent?” = Yes Complex Relevance Test @@ -108,8 +112,9 @@ ${age} >= 18 and ${consent} = 'yes' - + OR Relevance Test + Only if “Age” < 18 or “Do you consent?” = No OR Relevance Test @@ -117,17 +122,19 @@ ${age} < 18 or ${consent} = 'no' - + String Function Test + String Function Test string-length(${username}) > 3 string-length(.) >= 5 - - + + Math Function Test + Only if “Age” > 21 Math Function Test diff --git a/tests/fixtures/surveys/validation_relevance_survey/tsv.tsv b/tests/fixtures/surveys/validation_relevance_survey/tsv.tsv index d2c3e73..632a0c8 100644 --- a/tests/fixtures/surveys/validation_relevance_survey/tsv.tsv +++ b/tests/fixtures/surveys/validation_relevance_survey/tsv.tsv @@ -1,20 +1,20 @@ -class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip em_validation_q_tip -S language 1 en en -S format 1 A en -S shownoanswer 1 N en -SL surveyls_title 1 Validation and Relevance Test en -SL surveyls_welcometext 1 Validation and Relevance Test Survey en -G Questions 1 en -Q S username 1 Username en self != '' 1 Username cannot be empty -Q N age 1 Age en self >= 18 and self <= 120 1 Age must be between 18 and 120 -Q N price 1 Price en self > 0 and self < 1000 1 Price must be between 0 and 1000 -Q L consent 1 Do you consent? en 1 -A yes Yes en -A no No en -Q S adultinfo age >= 18 Adult Information en self != '' 1 -Q S seniordiscount age >= 65 Senior Discount Code en 1 -Q S consentrequiredinfo consent == 'yes' Additional Information en 1 -Q S complexrelevance age >= 18 and consent == 'yes' Complex Relevance Test en 1 -Q S orrelevance age < 18 or consent == 'no' OR Relevance Test en 1 -Q S stringfunctions strlen(username) > 3 String Function Test en strlen(self) >= 5 1 -Q N mathfunctions age > 21 Math Function Test en self >= age * 2 1 \ No newline at end of file +class type/scale name relevance text help language validation em_validation_q mandatory other default same_default hidden hide_tip num_value_int_only em_validation_q_tip +S language 1 en en +S format 1 A en +S shownoanswer 1 N en +SL surveyls_title 1 Validation and Relevance Test en +SL surveyls_welcometext 1 Validation and Relevance Test Survey en +G Questions 1 en +Q S username 1 Username en self != '' 1 Username cannot be empty +Q N age 1 Age en self >= 18 and self <= 120 1 1 Age must be between 18 and 120 +Q N price 1 Price en self > 0 and self < 1000 1 Price must be between 0 and 1000 +Q L consent 1 Do you consent? en 1 +A yes Yes en +A no No en +Q S adultinfo age >= 18 Adult Information en self != '' 1 +Q S seniordiscount age >= 65 Senior Discount Code en 1 +Q S consentrequiredinfo consent == 'yes' Additional Information en 1 +Q S complexrelevance age >= 18 and consent == 'yes' Complex Relevance Test en 1 +Q S orrelevance age < 18 or consent == 'no' OR Relevance Test en 1 +Q S stringfunctions strlen(username) > 3 String Function Test en strlen(self) >= 5 1 +Q N mathfunctions age > 21 Math Function Test en self >= age * 2 1 1 \ No newline at end of file diff --git a/tests/ts/contract/surveySnapshots.test.ts b/tests/ts/contract/surveySnapshots.test.ts index 1c495c5..e8d1a6a 100644 --- a/tests/ts/contract/surveySnapshots.test.ts +++ b/tests/ts/contract/surveySnapshots.test.ts @@ -247,10 +247,10 @@ const CASES = discoverCases(); type Row = Record; -/** Types the reverse spells differently by design: LimeSurvey has one numeric - * type (N → decimal) and `string` is XLSForm's alias of `text`. */ +/** Types the reverse spells differently by design: `int` and `string` are + * XLSForm's aliases of `integer` and `text`. */ const REVERSE_TYPE = (t: string): string => - ({ integer: 'decimal', int: 'decimal', string: 'text' })[t] ?? t; + ({ int: 'integer', string: 'text' })[t] ?? t; /** * Per question, what tsv → xlsform does not give back: `.: a -> b`, diff --git a/tests/ts/unit/ddi/fields.test.ts b/tests/ts/unit/ddi/fields.test.ts new file mode 100644 index 0000000..a52fdaa --- /dev/null +++ b/tests/ts/unit/ddi/fields.test.ts @@ -0,0 +1,232 @@ +/** + * The form's fields in the DDI (#153, convention:ddiFields): standard DDI + * where it has a home, a typed `cdl:` note where it has none. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, test, expect } from 'vitest'; + +import conventions from '../../../../src/generated/conventions.js'; +import { buildDdiXml } from '../../../../src/pipelines/xlsform2ddi/index.js'; + +const OPTS = { prodDate: '2020-01-01' }; + +const CHOICES = [ + { list_name: 'yn', name: 'yes', label: 'Yes' }, + { list_name: 'yn', name: 'no', label: 'No' }, + { list_name: 'yn', name: 'none', label: 'None', exclusive: 'yes' }, +]; + +function varXml(xml: string, name: string): string { + const start = xml.indexOf(`', start)); +} + +function varGrp(xml: string, id: string): string { + const start = xml.indexOf(`', start)); +} + +const ddi = (rows: Record[], settings = {}) => + buildDdiXml(rows, CHOICES, { ...OPTS, settings }); + +describe('standard DDI', () => { + test('date and time are varFormat categories', () => { + const xml = ddi([ + { type: 'date', name: 'd', label: 'D' }, + { type: 'time', name: 't', label: 'T' }, + { type: 'text', name: 's', label: 'S' }, + ]); + expect(varXml(xml, 'd')).toContain('category="date"'); + expect(varXml(xml, 't')).toContain('category="time"'); + expect(varXml(xml, 's')).not.toContain('category='); + }); + + test('an integer has no decimals; a decimal says nothing', () => { + const xml = ddi([ + { type: 'integer', name: 'i', label: 'I' }, + { type: 'decimal', name: 'x', label: 'X' }, + ]); + expect(varXml(xml, 'i')).toContain('dcml="0"'); + expect(varXml(xml, 'x')).not.toContain('dcml'); + }); + + test("a range's start and end are its valrng, the rest a note", () => { + const xml = ddi([ + { + type: 'range', + name: 'r', + label: 'R', + parameters: 'start=0 end=100 step=5', + }, + { type: 'range', name: 'plain', label: 'P' }, + ]); + const r = varXml(xml, 'r'); + expect(r).toContain(''); + expect(r).toContain('step=5'); + // The registry's defaults, as pyxform's. + expect(varXml(xml, 'plain')).toContain(''); + expect(varXml(xml, 'plain')).not.toContain('cdl:parameters'); + }); + + test('seqNo is the position among the data questions', () => { + const xml = ddi([ + { type: 'note', name: 'n', label: 'Intro' }, + { type: 'text', name: 'a', label: 'A' }, + { type: 'select_multiple yn', name: 'm', label: 'M' }, + { type: 'text', name: 'b', label: 'B' }, + ]); + expect(varXml(xml, 'a')).toContain('seqNo="1"'); + expect(varXml(xml, 'm_yes')).toContain('seqNo="2"'); + expect(varXml(xml, 'm_no')).toContain('seqNo="2"'); + expect(varXml(xml, 'b')).toContain('seqNo="3"'); + }); + + test('backward names the questions the condition refers to', () => { + const xml = ddi([ + { type: 'select_multiple yn', name: 'm', label: 'M' }, + { type: 'integer', name: 'k', label: 'K' }, + { + type: 'text', + name: 't', + label: 'T', + relevant: "selected(${m}, 'yes') and ${k} > 1 and ${k} < 9", + hint: 'H', + guidance_hint: 'G', + }, + ]); + const t = varXml(xml, 't'); + expect(t).toContain(''); + // XSD order in qstn. + expect(t.indexOf('')).toBeLessThan(t.indexOf('')); + }); + + test("a select_multiple's hints go with each option", () => { + const xml = ddi([ + { + type: 'select_multiple yn', + name: 'm', + label: 'M', + hint: 'All that apply', + guidance_hint: 'Read out', + }, + ]); + const v = varXml(xml, 'm_yes'); + expect(v).toContain('M'); + expect(v).toContain('All that apply'); + expect(v).toContain('Read out'); + }); +}); + +describe('cdl: notes', () => { + test('default, appearance and parameters on a question', () => { + const xml = ddi([ + { + type: 'select_one yn', + name: 'q', + label: 'Q', + default: 'no', + appearance: 'Minimal', + parameters: 'randomize=true; guidance_hint=Only once', + }, + ]); + const v = varXml(xml, 'q'); + expect(v).toContain('no'); + expect(v).toContain('minimal'); + // guidance_hint is its ivuInstr, not a parameter. + expect(v).toContain('randomize=true'); + expect(v).toContain('Only once'); + }); + + test("a select_multiple's on its varGrp, with its exclusive choices", () => { + const xml = ddi([ + { + type: 'select_multiple yn', + name: 'm', + label: 'M', + appearance: 'minimal', + }, + ]); + const grp = varGrp(xml, 'VG_m'); + expect(grp).toContain('minimal'); + expect(grp).toContain('none'); + expect(varXml(xml, 'm_yes')).not.toContain(' { + const xml = buildDdiXml( + [ + { + type: 'begin_group', + name: 'g', + 'label::Deutsch (de)': 'G', + 'label::English (en)': 'G', + 'hint::Deutsch (de)': 'Hinweis', + 'hint::English (en)': 'Hint', + appearance: 'field-list', + }, + { type: 'text', name: 't', 'label::Deutsch (de)': 'T' }, + { type: 'end_group' }, + { + type: 'begin_group', + name: 'grid', + 'label::Deutsch (de)': 'Grid', + appearance: 'table-list', + }, + { type: 'select_one yn', name: 'a', 'label::Deutsch (de)': 'A' }, + { type: 'end_group' }, + ], + CHOICES, + { ...OPTS, settings: { default_language: 'Deutsch (de)' } }, + ); + const g = varGrp(xml, 'VG_g'); + expect(g).toContain('Hinweis'); + expect(g).toContain('Hint'); + expect(g).toContain('field-list'); + // A grid's table-list is its type. + expect(varGrp(xml, 'VG_grid')).not.toContain('cdl:appearance'); + }); + + test('style is a cdl:setting', () => { + const xml = ddi([{ type: 'text', name: 't', label: 'T' }], { + style: 'pages', + }); + expect(xml).toContain( + 'pages', + ); + }); +}); + +describe('convention:ddiFields', () => { + const ROOT = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../..', + ); + const source = fs.readFileSync( + path.join(ROOT, 'src/instrument/types.ts'), + 'utf-8', + ); + + /** `Interface.field` for each field of the model's interfaces. */ + function modelFields(): string[] { + const out: string[] = []; + const re = /interface (\w+)(?: extends \w+)? \{([\s\S]*?)\n\}/g; + for (const [, name, body] of source.matchAll(re)) { + for (const [, field] of body.matchAll(/^ {2}(\w+)\??:/gm)) { + out.push(`${name}.${field}`); + } + } + return out; + } + + test('every Instrument field has a DDI home, a cdl: note or a documented loss', () => { + const mapped = Object.keys(conventions.conventions.ddiFields.fields); + const fields = modelFields(); + expect(fields.length).toBeGreaterThan(20); + expect(fields.filter((f) => !mapped.includes(f))).toEqual([]); + expect(mapped.filter((f) => !fields.includes(f))).toEqual([]); + }); +}); diff --git a/tests/ts/unit/ddi/multilingual.test.ts b/tests/ts/unit/ddi/multilingual.test.ts index 30f1ffa..28345d7 100644 --- a/tests/ts/unit/ddi/multilingual.test.ts +++ b/tests/ts/unit/ddi/multilingual.test.ts @@ -134,8 +134,8 @@ describe('xlsform → multilingual DDI', () => { [], { ...OPTS, settings: { default_language: 'Deutsch (de)' } }, ); - expect(xml).toContain('Aktuelle Tätigkeit'); - expect(xml).not.toContain('Aktuelle Tätigkeit'); + expect(xml).not.toContain(' { diff --git a/tests/ts/unit/pipelines/xlsform2ddi/codebook.test.ts b/tests/ts/unit/pipelines/xlsform2ddi/codebook.test.ts index 627e454..899962e 100644 --- a/tests/ts/unit/pipelines/xlsform2ddi/codebook.test.ts +++ b/tests/ts/unit/pipelines/xlsform2ddi/codebook.test.ts @@ -66,12 +66,12 @@ describe('file description', () => { }); describe('standalone scalar variables', () => { - test('integer → contin / numeric / numeric', () => { + test('integer → contin / numeric / numeric, no decimals', () => { const xml = build([{ type: 'integer', name: 'age', label: 'Age' }]); expect(xml).toContain( - '', + '', ); - expect(xml).toContain(''); + expect(xml).toContain(''); expect(xml).toContain(''); expect(xml).toContain('Age'); }); @@ -79,13 +79,15 @@ describe('standalone scalar variables', () => { test('text → discrete / text / character', () => { const xml = build([{ type: 'text', name: 't', label: 'Notes' }]); expect(xml).toContain('intrvl="discrete"'); - expect(xml).toContain(''); + expect(xml).toContain(''); }); test('a variable without a label emits no qstn', () => { const xml = build([{ type: 'integer', name: 'x' }]); - expect(xml).toContain(''); + expect(xml).toContain( + '', + ); expect(xml).not.toContain(' { { name: '2', label: 'Two' }, ], }); - expect(xml).toContain(''); + expect(xml).toContain('1'); expect(xml).toContain('One'); expect(count(xml, '')).toBe(2); @@ -127,7 +129,7 @@ describe('select_multiple → multipleResp expansion', () => { expect(xml).toContain( '', ); - expect(xml).toContain(''); + expect(xml).toContain('Devices'); expect(xml).toContain('Phone'); expect(xml).toContain('0'); @@ -243,7 +245,7 @@ describe('external code list (select_*_from_file)', () => { }, ]); expect(xml).toContain('Country'); - expect(xml).toContain(''); + expect(xml).toContain(''); }); }); diff --git a/tests/ts/unit/pipelines/xlsform2ddi/explicit.test.ts b/tests/ts/unit/pipelines/xlsform2ddi/explicit.test.ts index 1dd8be2..1fc190b 100644 --- a/tests/ts/unit/pipelines/xlsform2ddi/explicit.test.ts +++ b/tests/ts/unit/pipelines/xlsform2ddi/explicit.test.ts @@ -177,7 +177,7 @@ describe('notes', () => { expect(xml).not.toContain('Zu Medien'); }); - test('a grid member hint is dropped (its preQTxt is the grid text)', () => { + test("a grid member's hint is its postQTxt (its preQTxt is the grid text)", () => { const xml = buildDdiXml( [ { @@ -192,7 +192,7 @@ describe('notes', () => { ], yn, ); - expect(xml).not.toContain('Hinweis'); + expect(xml).toContain('Hinweis'); expect( [...xml.matchAll(/([^<]*)<\/preQTxt>/g)].map((m) => m[1]), ).toEqual(['Vertrauen', 'Vertrauen']); diff --git a/tests/ts/unit/pipelines/xlsform2ddi/hints.test.ts b/tests/ts/unit/pipelines/xlsform2ddi/hints.test.ts index 400c2e4..882e395 100644 --- a/tests/ts/unit/pipelines/xlsform2ddi/hints.test.ts +++ b/tests/ts/unit/pipelines/xlsform2ddi/hints.test.ts @@ -1,4 +1,4 @@ -/** hint → , guidance_hint → (#76). */ +/** hint → (#153), guidance_hint → (#76). */ import { describe, test, expect } from 'vitest'; import { buildDdiXml } from '../../../../../src/pipelines/xlsform2ddi/index.js'; @@ -22,7 +22,7 @@ const build = (survey: Row[], options = {}) => buildDdiXml(survey, choices, options); describe('xlsform → DDI hints', () => { - test('hint becomes preQTxt, guidance_hint becomes ivuInstr', () => { + test('hint becomes postQTxt, guidance_hint becomes ivuInstr', () => { const v = varXml( build([ { @@ -35,21 +35,24 @@ describe('xlsform → DDI hints', () => { ]), 'job', ); - expect(v).toContain('Aktueller Beruf'); + expect(v).toContain('Aktueller Beruf'); expect(v).toContain('Nicht vorlesen'); - expect(v.indexOf('')).toBeLessThan(v.indexOf('')); + expect(v).not.toContain(''); + // XSD order: qstnLit, postQTxt, ivuInstr. + expect(v.indexOf('')).toBeLessThan(v.indexOf('')); + expect(v.indexOf('')).toBeLessThan(v.indexOf('')); }); - test('no hint, no preQTxt or ivuInstr', () => { + test('no hint, no postQTxt or ivuInstr', () => { const v = varXml( build([{ type: 'text', name: 'job', label: 'Beruf?' }]), 'job', ); - expect(v).not.toContain(''); + expect(v).not.toContain(''); expect(v).not.toContain(''); }); - test("a preceding note's text comes first, then the hint", () => { + test('a preceding note is preQTxt, the hint stays apart as postQTxt', () => { const v = varXml( build([ { type: 'note', name: 'intro', label: 'Zu Ihrem Haushalt' }, @@ -57,7 +60,8 @@ describe('xlsform → DDI hints', () => { ]), 'n', ); - expect(v).toContain('Zu Ihrem Haushalt\n\nMit Kindern'); + expect(v).toContain('Zu Ihrem Haushalt'); + expect(v).toContain('Mit Kindern'); }); test('guidance_hint from parameters when there is no column', () => { @@ -90,7 +94,7 @@ describe('xlsform → DDI hints', () => { 'job', ); expect(v).toContain('Beruf?'); - expect(v).toContain('Aktueller Beruf'); + expect(v).toContain('Aktueller Beruf'); }); }); @@ -100,7 +104,7 @@ describe("validateSubset — 'hint-dropped'", () => { .filter((d) => d.code === 'hint-dropped') .map((d) => d.name); - test('ddi: a select_multiple with a hint or guidance_hint', () => { + test('ddi: none, every hint has a place (#153)', () => { const survey = [ { type: 'select_multiple yn', name: 'm', label: 'M?', hint: 'H' }, { type: 'select_one yn', name: 'o', label: 'O?', hint: 'H' }, @@ -111,7 +115,7 @@ describe("validateSubset — 'hint-dropped'", () => { parameters: 'guidance_hint=G', }, ]; - expect(dropped(survey, 'ddi')).toEqual(['m', 'p']); + expect(dropped(survey, 'ddi')).toEqual([]); }); test('lstsv: any guidance_hint, not a plain hint', () => { diff --git a/tests/validation/test_registry_schematron_conformance.py b/tests/validation/test_registry_schematron_conformance.py index 89f5626..7c909d7 100644 --- a/tests/validation/test_registry_schematron_conformance.py +++ b/tests/validation/test_registry_schematron_conformance.py @@ -238,7 +238,7 @@ def mutate(xml: str) -> str | None: ASSERT_MUTATIONS = [ ("composite:grid", _dup_first("varGrp"), "Duplicate Variable Group ID"), ("type:select_one", _sub1(r'(]*?)\s+intrvl="[^"]+"', r"\1"), "missing an intrvl attribute"), - ("type:select_one", _sub1(r'', ""), "missing responseDomainType"), + ("type:select_one", _sub1(r'.*?", ""), "missing a question literal"), ("type:select_one", _sub1(r"]*/>", ""), "missing technical format"), ( @@ -272,12 +272,12 @@ def mutate(xml: str) -> str | None: ), ( "type:select_multiple", - _sub1(r'', ''), + _sub1(r'', ''), + _sub1(r'