diff --git a/anet-dictionary.yml b/anet-dictionary.yml index b8a9149c3b..3f35387175 100644 --- a/anet-dictionary.yml +++ b/anet-dictionary.yml @@ -379,6 +379,14 @@ fields: label: Location placeholder: Search for the engagement location… filter: [POINT_LOCATION, VIRTUAL_LOCATION] + reportPeople: + optionalAttendingAuthor: false + optionalPrimaryAdvisor: false + optionalPrimaryPrincipal: true + attendeeGroups: + - label: Linguists + filter: + orgUuid: 70193ee9-05b4-4aac-80b5-75609825db9f customFields: relatedReport: type: anet_object @@ -508,10 +516,6 @@ fields: typeError: Qty must be a number label: Qty visibleWhen: $[?(@ && @.multipleButtons && (@.multipleButtons.indexOf('assist') != -1 || @.multipleButtons.indexOf('other') != -1))] - attendeeGroups: - - label: Linguists - filter: - orgUuid: 70193ee9-05b4-4aac-80b5-75609825db9f person: status: diff --git a/client/src/models/Report.js b/client/src/models/Report.js index 6e3a159a8d..be1a3b9b6c 100644 --- a/client/src/models/Report.js +++ b/client/src/models/Report.js @@ -162,20 +162,10 @@ export default class Report extends Model { .array() .nullable() .when("cancelled", ([cancelled], schema) => + // Only do validation warning when engagement not cancelled cancelled - ? schema.nullable() - : schema // Only do validation warning when engagement not cancelled - .test( - "primary-advisor", - "primary advisor error", - (reportPeople, testContext) => { - const message = Report.checkPrimaryAttendee( - reportPeople, - Person.ROLE.ADVISOR - ) - return message ? testContext.createError({ message }) : true - } - ) + ? schema + : Report.testPrimaryAttendees(schema, false) .test( "no-author", "no author error", @@ -281,20 +271,8 @@ export default class Report extends Model { .array() .nullable() .when("cancelled", ([cancelled], schema) => - cancelled - ? schema.nullable() - : schema // Only do validation warning when engagement not cancelled - .test( - "primary-principal", - "primary principal error", - (reportPeople, testContext) => { - const message = Report.checkPrimaryAttendee( - reportPeople, - Person.ROLE.PRINCIPAL - ) - return message ? testContext.createError({ message }) : true - } - ) + // Only do validation warning when engagement not cancelled + cancelled ? schema : Report.testPrimaryAttendees(schema, true) ), reportSensitiveInformation: yup.object().nullable().default({}), authorizationGroups: yup @@ -317,6 +295,36 @@ export default class Report extends Model { ) }) + static testPrimaryAttendees(schema, asWarning) { + return schema + .test( + "primary-advisor", + "primary advisor error", + (reportPeople, testContext) => { + const message = Report.checkPrimaryAttendee( + reportPeople, + Person.ROLE.ADVISOR, + Settings.fields.report.reportPeople?.optionalPrimaryAdvisor, + asWarning + ) + return message ? testContext.createError({ message }) : true + } + ) + .test( + "primary-principal", + "primary principal error", + (reportPeople, testContext) => { + const message = Report.checkPrimaryAttendee( + reportPeople, + Person.ROLE.PRINCIPAL, + Settings.fields.report.reportPeople?.optionalPrimaryPrincipal, + asWarning + ) + return message ? testContext.createError({ message }) : true + } + ) + } + static autocompleteQuery = "uuid, intent, authors { uuid, name, rank, role }" constructor(props) { @@ -404,13 +412,13 @@ export default class Report extends Model { return this.intent || "None" } - static checkPrimaryAttendee(reportPeople, role) { + static checkPrimaryAttendee(reportPeople, role, optional, asWarning) { const primaryAttendee = Report.getPrimaryAttendee(reportPeople, role) const roleName = Person.humanNameOfRole(role) - if (!primaryAttendee && role === Person.ROLE.ADVISOR) { - return `You must provide the primary ${roleName} for the Engagement` - } else if (!primaryAttendee && role === Person.ROLE.PRINCIPAL) { - return `No primary ${roleName} has been provided for the Engagement` + if (!primaryAttendee) { + if ((optional && asWarning) || (!optional && !asWarning)) { + return `No primary ${roleName} has been provided for the Engagement` + } } else if (primaryAttendee.status !== Model.STATUS.ACTIVE) { return `The primary ${roleName} - ${primaryAttendee.name} - needs to have an active profile` } else if ( @@ -426,8 +434,11 @@ export default class Report extends Model { } static checkAttendingAuthor(reportPeople) { - if (!reportPeople?.some(rp => rp.author && rp.attendee)) { - return "You must provide at least 1 attending author" + const optionalAttendingAuthor = + Settings.fields.report.reportPeople?.optionalAttendingAuthor + const attendingAuthor = reportPeople?.some(rp => rp.author && rp.attendee) + if (!attendingAuthor && !optionalAttendingAuthor) { + return "You must provide at least 1 attending author(s)" } } diff --git a/client/src/pages/reports/Form.js b/client/src/pages/reports/Form.js index 948e2ef14a..d35b364182 100644 --- a/client/src/pages/reports/Form.js +++ b/client/src/pages/reports/Form.js @@ -366,7 +366,8 @@ const ReportForm = ({ } // Add attendee groups defined in the dictionary - const attendeeGroups = Settings.fields.report.attendeeGroups ?? [] + const attendeeGroups = + Settings.fields.report.reportPeople?.attendeeGroups ?? [] attendeeGroups.forEach(({ label, filter: queryVars }) => { reportPeopleFilters[label] = { label, queryVars } }) diff --git a/src/main/java/mil/dds/anet/resources/ReportResource.java b/src/main/java/mil/dds/anet/resources/ReportResource.java index baf449c13d..de32db2f27 100644 --- a/src/main/java/mil/dds/anet/resources/ReportResource.java +++ b/src/main/java/mil/dds/anet/resources/ReportResource.java @@ -398,15 +398,26 @@ public int submitReport(@GraphQLRootContext Map context, if (r.getAdvisorOrgUuid() == null) { final ReportPerson advisor = r.loadPrimaryAdvisor(engine.getContext()).join(); + final Boolean optionalPrimaryAdvisor = + (Boolean) config.getDictionaryEntry("fields.report.reportPeople.optionalPrimaryAdvisor"); if (advisor == null) { - throw new WebApplicationException("Report missing primary advisor", Status.BAD_REQUEST); + if (!Boolean.TRUE.equals(optionalPrimaryAdvisor)) { + throw new WebApplicationException("Report missing primary advisor", Status.BAD_REQUEST); + } + } else { + r.setAdvisorOrg( + engine.getOrganizationForPerson(engine.getContext(), advisor.getUuid()).join()); } - r.setAdvisorOrg( - engine.getOrganizationForPerson(engine.getContext(), advisor.getUuid()).join()); } if (r.getPrincipalOrgUuid() == null) { final ReportPerson principal = r.loadPrimaryPrincipal(engine.getContext()).join(); - if (principal != null) { + final Boolean optionalPrimaryPrincipal = (Boolean) config + .getDictionaryEntry("fields.report.reportPeople.optionalPrimaryPrincipal"); + if (principal == null) { + if (!Boolean.TRUE.equals(optionalPrimaryPrincipal)) { + throw new WebApplicationException("Report missing primary principal", Status.BAD_REQUEST); + } + } else { r.setPrincipalOrg( engine.getOrganizationForPerson(engine.getContext(), principal.getUuid()).join()); } diff --git a/src/main/resources/anet-schema.yml b/src/main/resources/anet-schema.yml index 5071055439..9d52e06c9e 100644 --- a/src/main/resources/anet-schema.yml +++ b/src/main/resources/anet-schema.yml @@ -640,29 +640,44 @@ properties: type: string enum: [VIRTUAL_LOCATION, PHYSICAL_LOCATION, GEOGRAPHICAL_AREA, POINT_LOCATION, ADVISOR_LOCATION, PRINCIPAL_LOCATION] + reportPeople: + required: [optionalAttendingAuthor, optionalPrimaryAdvisor, optionalPrimaryPrincipal] + properties: + optionalAttendingAuthor: + type: boolean + default: false + description: Defines if having an attending author for a report is optional + optionalPrimaryAdvisor: + type: boolean + default: false + description: Defines if having a primary advisor for a report is optional + optionalPrimaryPrincipal: + type: boolean + default: false + description: Defines if having a primary principal for a report is optional + attendeeGroups: + type: array + uniqueItems: true + items: + type: object + additionalProperties: false + title: Attendee-group + required: [label, filter] + properties: + label: + type: string + title: The label of the attendee group + filter: + type: object + title: Attendee group filters + properties: + orgUuid: + type: string + title: UUID of organisation membership to form customFields: type: object additionalProperties: "$ref": "#/$defs/customField" - attendeeGroups: - type: array - uniqueItems: true - items: - type: object - additionalProperties: false - title: Attendee-group - required: [label, filter] - properties: - label: - type: string - title: The label of the attendee group - filter: - type: object - title: Attendee group filters - properties: - orgUuid: - type: string - title: UUID of organisation membership to form person: type: object diff --git a/testDictionaries/no-custom-fields.yml b/testDictionaries/no-custom-fields.yml index 99fafef87f..089654a805 100644 --- a/testDictionaries/no-custom-fields.yml +++ b/testDictionaries/no-custom-fields.yml @@ -363,10 +363,14 @@ fields: label: Location placeholder: Search for the engagement location… filter: [POINT_LOCATION, VIRTUAL_LOCATION] - attendeeGroups: - - label: Linguists - filter: - orgUuid: 70193ee9-05b4-4aac-80b5-75609825db9f + reportPeople: + optionalAttendingAuthor: false + optionalPrimaryAdvisor: false + optionalPrimaryPrincipal: true + attendeeGroups: + - label: Linguists + filter: + orgUuid: 70193ee9-05b4-4aac-80b5-75609825db9f person: status: