Skip to content

Commit

Permalink
UIQM-698 Validate 006/007 field lengths. (#735)
Browse files Browse the repository at this point in the history
* UIQM-698 Validate 006/007 field lengths.

* UIQM-698 update tests
  • Loading branch information
BogdanDenis authored Sep 26, 2024
1 parent 04d6cb7 commit 8d9a187
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* [UIQM-693](https://issues.folio.org/browse/UIQM-693) Hide permission - Edit, View: Enable duplicate LCCN (010 $a) checking of MARC bibliographic and authority records.
* [UIQM-695](https://issues.folio.org/browse/UIQM-695) Remove extra `$` from error messages when adding/removing `$t` from 1XX of linked MARC authority record.
* [UIQM-706](https://issues.folio.org/browse/UIQM-706) *BREAKING* Upgrade `marc-records-editor` to `6.0`.
* [UIQM-698](https://issues.folio.org/browse/UIQM-698) Validate 006/007 field lengths.

## [8.0.1] (https://github.com/folio-org/ui-quick-marc/tree/v8.0.1) (2024-04-18)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,6 @@ const QuickMarcEditorRows = ({
marcType={marcType}
leaderField={recordRow}
action={action}
error={fieldValidationIssues}
/>
)}
{
Expand Down
23 changes: 23 additions & 0 deletions src/QuickMarcEditor/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1234,3 +1234,26 @@ export const getVisibleNonSelectable008Subfields = (fixedFieldType) => {
.filter(field => !field.readOnly)
.filter(field => !field.isArray);
};

export const getFixedFieldStringPositions = (type, subtype, field, fixedFieldSpec) => {
if (isFixedFieldRow(field)) {
const fixedFieldType = FixedFieldFactory.getFixedFieldType(fixedFieldSpec, type, subtype);
const nonSelectableSubfields = getVisibleNonSelectable008Subfields(fixedFieldType);

return nonSelectableSubfields;
}

if (isMaterialCharsRecord(field)) {
const materialCharsConfig = getMaterialCharsFieldConfig(field.content.Type);

return materialCharsConfig.filter(item => item.type === SUBFIELD_TYPES.STRING);
}

if (isPhysDescriptionRecord(field)) {
const materialCharsConfig = getPhysDescriptionFieldConfig(field.content.Category);

return materialCharsConfig.filter(item => item.type === SUBFIELD_TYPES.STRING);
}

return [];
};
62 changes: 61 additions & 1 deletion src/QuickMarcEditor/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable max-lines */
import faker from 'faker';

import { v4 as uuid } from 'uuid';

import { FixedFieldFactory } from './QuickMarcEditorRows/FixedField';
import {
LEADER_TAG,
QUICK_MARC_ACTIONS,
Expand All @@ -19,6 +20,7 @@ import {
bibLeaderString,
holdingsLeader,
} from '../../test/jest/fixtures/leaders';
import { SUBFIELD_TYPES } from './QuickMarcEditorRows/BytesField';

jest.mock('uuid', () => {
return {
Expand Down Expand Up @@ -1811,4 +1813,62 @@ describe('QuickMarcEditor utils', () => {
});
});
});

describe('getFixedFieldStringPositions', () => {
beforeEach(() => {
jest.spyOn(FixedFieldFactory, 'getFixedFieldType').mockReturnValue({
items: [{
code: 'test1',
isArray: true,
}, {
code: 'test2',
isArray: false,
readOnly: false,
}],
});
});

describe('when a field is an 008', () => {
it('should return an 008 config', () => {
const field = { tag: '008' };

expect(utils.getFixedFieldStringPositions('a', 'm', field, fixedFieldSpecBib)).toEqual([
{
code: 'test2',
isArray: false,
readOnly: false,
},
]);
});
});

describe('when a field is an 007', () => {
it('should return an 007 config', () => {
const field = { tag: '007', content: { Category: 'c' } };

expect(utils.getFixedFieldStringPositions('a', 'm', field, fixedFieldSpecBib)).toEqual([
{
name: 'Image bit depth',
type: SUBFIELD_TYPES.STRING,
length: 3,
},
]);
});
});

describe('when a field is an 006', () => {
it('should return an 006 config', () => {
const field = { tag: '006', content: { Type: 'f' } };

expect(utils.getFixedFieldStringPositions('f', 'm', field, fixedFieldSpecBib)).toEqual([
{
name: 'Proj',
hint: 'Projection',
type: SUBFIELD_TYPES.STRING,
length: 2,
},
]);
});
});
});
});
24 changes: 22 additions & 2 deletions src/hooks/useValidation/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,20 @@ const BASE_BIB_VALIDATORS = [
validator: RULES.FIXED_FIELD_POSITIONS,
message: (name) => ({ id: 'ui-quick-marc.record.error.008.invalidValue', values: { name } }),
},
{
tag: '006',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
{
tag: '007',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
{
tag: '008',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.008.invalidLength', values: { name, length } }),
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
{
validator: RULES.$9IN_LINKABLE,
Expand Down Expand Up @@ -265,10 +275,20 @@ const BASE_AUTHORITY_VALIDATORS = [
validator: RULES.DUPLICATE_LCCN,
message: () => ({ id: 'ui-quick-marc.record.error.010.lccnDuplicated' }),
},
{
tag: '006',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
{
tag: '007',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
{
tag: '008',
validator: RULES.FIXED_FIELD_LENGTH,
message: (name, length) => ({ id: 'ui-quick-marc.record.error.008.invalidLength', values: { name, length } }),
message: (name, length) => ({ id: 'ui-quick-marc.record.error.fixedField.invalidLength', values: { name, length } }),
},
];

Expand Down
30 changes: 14 additions & 16 deletions src/hooks/useValidation/useValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@ import {
useValidate,
} from '../../queries';
import {
getFixedFieldStringPositions,
getLeaderPositions,
getVisibleNonSelectable008Subfields,
isFixedFieldRow,
isLeaderRow,
isMaterialCharsRecord,
isPhysDescriptionRecord,
joinErrors,
} from '../../QuickMarcEditor/utils';
import { MARC_TYPES } from '../../common/constants';
import {
MISSING_FIELD_ID,
SEVERITY,
} from './constants';
import {
FIXED_FIELD_TAG,
QUICK_MARC_ACTIONS,
} from '../../QuickMarcEditor/constants';
import { FixedFieldFactory } from '../../QuickMarcEditor/QuickMarcEditorRows/FixedField';
import { QUICK_MARC_ACTIONS } from '../../QuickMarcEditor/constants';

const BE_VALIDATION_MARC_TYPES = [MARC_TYPES.BIB, MARC_TYPES.AUTHORITY];

Expand Down Expand Up @@ -112,24 +111,23 @@ const useValidation = (context = {}, tenantId = null) => {
return issues;
}, [context.action, context.marcType]);

// if the length of a subfield of field 008 is shorter, then add backslashes,
// if the length of a subfield of a fixed field is shorter, then add backslashes,
// if longer, then cut off the extra characters.
const fillIn008FieldBlanks = useCallback((marcRecords) => {
const fillInFixedFieldBlanks = useCallback((marcRecords) => {
if (![MARC_TYPES.BIB, MARC_TYPES.AUTHORITY].includes(context.marcType)) {
return marcRecords;
}

const { type, position7 } = getLeaderPositions(context.marcType, marcRecords);
const fixedFieldType = FixedFieldFactory.getFixedFieldType(context.fixedFieldSpec, type, position7);

const fieldsMap = getVisibleNonSelectable008Subfields(fixedFieldType)
.reduce((acc, field) => ({ ...acc, [field.code]: field }), {});
const { type, position7: subtype } = getLeaderPositions(context.marcType, marcRecords);

return marcRecords.map(field => {
if (field.tag !== FIXED_FIELD_TAG) {
if (!isFixedFieldRow(field) && !isMaterialCharsRecord(field) && !isPhysDescriptionRecord(field)) {
return field;
}

const fieldsMap = getFixedFieldStringPositions(type, subtype, field, context.fixedFieldSpec)
.reduce((acc, _field) => ({ ...acc, [_field.code || _field.name]: _field }), {});

// if the spec contains a subfield length of 4, then '123456' becomes '1234' and '12' becomes '12\\\\'
return {
...field,
Expand All @@ -153,7 +151,7 @@ const useValidation = (context = {}, tenantId = null) => {
}, [context.fixedFieldSpec, context.marcType]);

const runBackEndValidation = useCallback(async (records) => {
const marcRecords = fillIn008FieldBlanks(records);
const marcRecords = fillInFixedFieldBlanks(records);

const body = {
fields: marcRecords.filter(record => !isLeaderRow(record)),
Expand All @@ -167,7 +165,7 @@ const useValidation = (context = {}, tenantId = null) => {
() => formatBEValidationResponse(response, marcRecords),
removeError001MissingField,
)();
}, [context, validateFetch, removeError001MissingField, fillIn008FieldBlanks]);
}, [context, validateFetch, removeError001MissingField, fillInFixedFieldBlanks]);

const isBackEndValidationMarcType = useCallback(marcType => BE_VALIDATION_MARC_TYPES.includes(marcType), []);

Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useValidation/useValidation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,22 +626,22 @@ describe('useValidation', () => {

expect(validationErrors).toEqual(expect.objectContaining({
5: [{
id: 'ui-quick-marc.record.error.008.invalidLength',
id: 'ui-quick-marc.record.error.fixedField.invalidLength',
severity: 'error',
values: {
length: 4,
name: 'ui-quick-marc.record.fixedField.Date1',
},
}, {
id: 'ui-quick-marc.record.error.008.invalidLength',
id: 'ui-quick-marc.record.error.fixedField.invalidLength',
severity: 'error',
values: {
length: 3,
name: 'ui-quick-marc.record.fixedField.Ctry',
},
}],
6: [{
id: 'ui-quick-marc.record.error.008.invalidLength',
id: 'ui-quick-marc.record.error.fixedField.invalidLength',
severity: 'error',
values: {
length: 4,
Expand Down Expand Up @@ -1023,14 +1023,14 @@ describe('useValidation', () => {
]);

expect(validationErrors[4]).toEqual([{
id: 'ui-quick-marc.record.error.008.invalidLength',
id: 'ui-quick-marc.record.error.fixedField.invalidLength',
severity: 'error',
values: {
length: 1,
name: 'ui-quick-marc.record.fixedField.Geo Subd',
},
}, {
id: 'ui-quick-marc.record.error.008.invalidLength',
id: 'ui-quick-marc.record.error.fixedField.invalidLength',
severity: 'error',
values: {
length: 1,
Expand Down
17 changes: 10 additions & 7 deletions src/hooks/useValidation/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
checkIsEmptyContent,
convertLeaderToString,
getLeaderPositions,
getVisibleNonSelectable008Subfields,
getFixedFieldStringPositions,
} from '../../QuickMarcEditor/utils';
import {
LEADER_EDITABLE_BYTES,
Expand Down Expand Up @@ -412,16 +412,19 @@ export const validateFixedFieldPositions = ({ marcRecords, fixedFieldSpec, marcT

return undefined;
};

export const validateFixedFieldLength = ({ marcRecords, fixedFieldSpec, marcType, intl }, rule) => {
const { type, position7: subtype } = getLeaderPositions(marcType, marcRecords);
const fixedFieldType = FixedFieldFactory.getFixedFieldType(fixedFieldSpec, type, subtype);
const fields008 = marcRecords.filter(x => x.tag === FIXED_FIELD_TAG);
const nonSelectableSubfields = getVisibleNonSelectable008Subfields(fixedFieldType);
const fieldsToCheck = marcRecords.filter(x => x.tag === rule.tag);

const errors = fieldsToCheck.reduce((acc, field) => {
const nonSelectableSubfields = getFixedFieldStringPositions(type, subtype, field, fixedFieldSpec);

const errors = fields008.reduce((acc, field) => {
nonSelectableSubfields.forEach(subfield => {
if (field.content[subfield.code] && field.content[subfield.code].length !== subfield.length) {
const subfieldName = intl.formatMessage({ id: `ui-quick-marc.record.fixedField.${subfield.code}` });
const content = field.content[subfield.code || subfield.name];

if (content && content.length !== subfield.length) {
const subfieldName = intl.formatMessage({ id: `ui-quick-marc.record.fixedField.${subfield.code || subfield.name}` });

acc[field.id] = [
...(acc[field.id] || []),
Expand Down
2 changes: 1 addition & 1 deletion translations/ui-quick-marc/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@
"record.error.008.empty": "Record cannot be saved without 008 field",
"record.error.008.multiple": "Record cannot be saved with more than one 008 field",
"record.error.008.invalidValue": "Record cannot be saved. Field 008 contains an invalid value in \"{name}\" position.",
"record.error.008.invalidLength": "Invalid {name} field length, must be {length} characters.",
"record.error.fixedField.invalidLength": "Invalid {name} field length, must be {length} characters.",
"record.error.heading.empty": "Record cannot be saved without 1XX field.",
"record.error.heading.multiple": "Record cannot be saved. Cannot have multiple 1XXs",
"record.error.title.multiple": "Record cannot be saved with more than one field 245.",
Expand Down

0 comments on commit 8d9a187

Please sign in to comment.