{{'loading-units' | translate}}
+| + |
+ |
+ {{'coding.unit-name' | translate}} | +{{formatUnitName(unit.unitName)}} | +
|---|---|
| + @if (filterValue) { + {{'no-units-matching' | translate}} "{{filterValue}}" + } @else { + {{'no-units-available' | translate}} + } + | +
Kodierschema mit Schemer Version ab 1.5 erzeugen!
' + }; + if (contentSetting.showScore) codeInfo.score = ''; + return codeInfo; + } + + private static getCodeInfoFromCodeAsText(code: CodeData, contentSetting: CodeBookContentSetting): CodeInfo { + const codeAsText = ToTextFactory.codeAsText(code, 'SIMPLE'); + const rulesDescription = contentSetting.hasOnlyManualCoding && !contentSetting.hasClosedVars ? '' : + this.getRulesDescription(codeAsText, code); + const codeInfo: CodeInfo = { + id: `${code.id}`, + label: contentSetting.codeLabelToUpper ? codeAsText.label.toUpperCase() : codeAsText.label, + description: `${rulesDescription}${code.manualInstruction ?? ''}` + }; + if (contentSetting.showScore) codeInfo.score = codeAsText.score.toString(); + return codeInfo; + } + + private static getRulesDescription(codeAsText: CodeAsText, code: CodeData): string { + let rulesDescription = ''; + codeAsText.ruleSetDescriptions.forEach( + (ruleSetDescription: string) => { + if (ruleSetDescription !== 'Keine Regeln definiert.') { + rulesDescription += `${ruleSetDescription}
`; + } else if ((code.manualInstruction ?? '') === '') rulesDescription += `${ruleSetDescription}
`; + } + ); + return rulesDescription; + } +} diff --git a/projects/ngx-coding-components/codebook-generator/src/public-api.ts b/projects/ngx-coding-components/codebook-generator/src/public-api.ts new file mode 100644 index 0000000..37b2655 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/src/public-api.ts @@ -0,0 +1,11 @@ +export * from './lib/codebook-generator/codebook-generator.class'; +export * from './lib/codebook-generator/codebook-docx-generator.class'; +export type { + BookVariable, + CodeBookContentSetting, + CodebookUnitDto, + CodeInfo, + ItemMetadata, + Missing, + UnitPropertiesForCodebook +} from '@iqb/ngx-coding-components/codebook-models'; diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index d9b47a1..9396ed9 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -62,8 +62,18 @@ "ngx-build-plus": "^20.0.0", "prosemirror-state": "^1.3.4", "rxjs": "~7.8.0", + "docx": "^8.5.0", + "cheerio": "^1.0.0", "zone.js": "~0.15.0" }, + "peerDependenciesMeta": { + "docx": { + "optional": true + }, + "cheerio": { + "optional": true + } + }, "devDependencies": { "@angular-devkit/build-angular": "^20.0.4", "@angular/compiler-cli": "^20.0.5", diff --git a/projects/ngx-coding-components/src/lib/codebook-generator/codebook-generator.class.spec.ts b/projects/ngx-coding-components/src/lib/codebook-generator/codebook-generator.class.spec.ts new file mode 100644 index 0000000..7297ba9 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/codebook-generator/codebook-generator.class.spec.ts @@ -0,0 +1,176 @@ +import JSZip from 'jszip'; + +import { + CodebookGenerator +} from '../../../codebook-generator/src/lib/codebook-generator/codebook-generator.class'; +import { + CodeBookContentSetting +} from '../../../codebook-models/src/lib/codebook.interfaces'; + +describe('CodebookGenerator', () => { + const contentSetting: CodeBookContentSetting = { + exportFormat: 'json', + missingsProfile: '', + hasOnlyManualCoding: false, + hasGeneralInstructions: true, + hasDerivedVars: true, + hasOnlyVarsWithCodes: true, + hasClosedVars: true, + codeLabelToUpper: false, + showScore: true, + hideItemVarRelation: true + }; + + it('keeps numeric code id 0 in JSON exports', async () => { + const scheme = JSON.stringify({ + version: '1.5', + variableCodings: [ + { + id: 'VAR1', + sourceType: 'BASE', + label: 'Variable 1', + codes: [ + { + id: 0, + type: 'RESIDUAL_AUTO', + label: 'Falsch', + score: 0, + ruleSetOperatorAnd: true, + ruleSets: [], + manualInstruction: '' + } + ] + } + ] + }); + + const blob = await CodebookGenerator.generateCodebook( + [ + { + id: 1, + key: 'UNIT1', + name: 'Unit 1', + scheme + } + ], + contentSetting, + [] + ); + const codebook = JSON.parse(await blob.text()); + + expect(blob).toEqual(jasmine.any(Blob)); + expect(codebook[0].variables[0].codes[0].id).toBe('0'); + }); + + it('returns a JSON Blob for empty exports', async () => { + const blob = await CodebookGenerator.generateCodebook([], contentSetting, []); + + expect(blob).toEqual(jasmine.any(Blob)); + expect(blob.type).toBe('application/json'); + expect(await blob.text()).toBe('[]'); + }); + + it('filters closed codes without dropping variables that still have included codes', async () => { + const scheme = JSON.stringify({ + version: '1.5', + variableCodings: [ + { + id: 'VAR1', + sourceType: 'BASE', + label: 'Variable 1', + codes: [ + { + id: 1, + type: 'FULL_CREDIT', + label: 'Manuell', + score: 1, + ruleSetOperatorAnd: true, + ruleSets: [], + manualInstruction: 'Manual instruction
' + }, + { + id: 0, + type: 'RESIDUAL_AUTO', + label: 'Automatisch', + score: 0, + ruleSetOperatorAnd: true, + ruleSets: [], + manualInstruction: '' + } + ] + } + ] + }); + + const blob = await CodebookGenerator.generateCodebook( + [ + { + id: 1, + key: 'UNIT1', + name: 'Unit 1', + scheme + } + ], + { + ...contentSetting, + hasClosedVars: false + }, + [] + ); + const codebook = JSON.parse(await blob.text()); + + expect(codebook[0].variables.length).toBe(1); + expect(codebook[0].variables[0].codes.map((code: { id: string }) => code.id)).toEqual(['1']); + }); + + it('writes ordered lists as numbering and normalizes HTML whitespace in DOCX exports', async () => { + const scheme = JSON.stringify({ + version: '1.5', + variableCodings: [ + { + id: 'VAR1', + sourceType: 'BASE', + label: 'Variable 1', + codes: [ + { + id: 1, + type: 'FULL_CREDIT', + label: 'Manuell', + score: 1, + ruleSetOperatorAnd: true, + ruleSets: [], + manualInstruction: 'Description text
' + } + ] + } + ] + }; + + const blob = await CodebookDocxGenerator.generateDocx( + [unit], + { + ...contentSetting, + exportFormat: 'docx', + hideItemVarRelation: false + } + ); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + const documentXml = await zip.file('word/document.xml')?.async('string'); + + expect(documentXml).toContain('UNIT1 Unit 1'); + expect(documentXml).toContain('VAR1 Variable 1'); + expect(documentXml).toContain('Item(s): ITEM1 ITEM2'); + expect(documentXml).not.toContain('ITEM3'); + + const scoreHeaderIndex = documentXml?.indexOf('Score') ?? -1; + const descriptionHeaderIndex = documentXml?.indexOf('Beschreibung') ?? -1; + const scoreValueIndex = documentXml?.indexOf('77') ?? -1; + const descriptionValueIndex = documentXml?.indexOf('Description text') ?? -1; + + expect(scoreHeaderIndex).toBeGreaterThan(-1); + expect(descriptionHeaderIndex).toBeGreaterThan(-1); + expect(scoreHeaderIndex).toBeLessThan(descriptionHeaderIndex); + expect(scoreValueIndex).toBeGreaterThan(-1); + expect(descriptionValueIndex).toBeGreaterThan(-1); + expect(scoreValueIndex).toBeLessThan(descriptionValueIndex); + }); }); From 2a77005c2dcb46eafa60aeb0469ccab77b13fe21 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:39:18 +0200 Subject: [PATCH 7/8] Handle codebook DOCX edge cases --- .../codebook-docx-generator.class.ts | 65 ++++++++------ .../codebook-generator.class.ts | 3 - .../codebook-generator.class.spec.ts | 87 +++++++++++++++++++ 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/projects/ngx-coding-components/codebook-generator/src/lib/codebook-generator/codebook-docx-generator.class.ts b/projects/ngx-coding-components/codebook-generator/src/lib/codebook-generator/codebook-docx-generator.class.ts index e0cb190..2417e42 100644 --- a/projects/ngx-coding-components/codebook-generator/src/lib/codebook-generator/codebook-docx-generator.class.ts +++ b/projects/ngx-coding-components/codebook-generator/src/lib/codebook-generator/codebook-docx-generator.class.ts @@ -18,7 +18,7 @@ import { import * as cheerio from 'cheerio'; import type { AnyNode, Element } from 'domhandler'; import type { - BookVariable, CodeBookContentSetting, CodebookUnitDto, ItemMetadata + BookVariable, CodeBookContentSetting, CodebookUnitDto, ItemMetadata, Missing } from '@iqb/ngx-coding-components/codebook-models'; /** @@ -35,27 +35,23 @@ export class CodebookDocxGenerator { codingBookUnits: CodebookUnitDto[], contentSetting: CodeBookContentSetting ): Promise