Skip to content

Commit

Permalink
feat(lunatic): format control for the year of date questions (#1168)
Browse files Browse the repository at this point in the history
* feat(lunatic): format control for the year of date questions

* refactor: some java 21+ syntax refactor

* refactor: some clean up in test

* chore: bump version

* feat: add 'at least 4 digits' in control
  • Loading branch information
nsenave authored Dec 6, 2024
1 parent b6f918c commit 6cc98e1
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 44 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ java {

allprojects {
group = "fr.insee.eno"
version = "3.29.3-SNAPSHOT"
version = "3.30.0-SNAPSHOT.1"
}

subprojects {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import fr.insee.eno.core.processing.ProcessingStep;
import fr.insee.lunatic.model.flat.*;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.RoundingMode;
Expand All @@ -14,6 +15,7 @@
* Processing adding format controls to components
* Format controls are controls generated by the min/max/decimals attributes of the Datepicker/InputNumber components
*/
@Slf4j
public class LunaticAddControlFormat implements ProcessingStep<Questionnaire> {
/**
*
Expand Down Expand Up @@ -114,21 +116,21 @@ private List<ControlType> getFormatControlsFromInputNumberAttributes(String id,
String maxValue = formatDoubleValue(max, decimalsCount);
String controlExpression = String.format("not(not(isnull(%s)) and (%s>%s or %s<%s))", responseName, minValue, responseName, maxValue, responseName);
String controlErrorMessage = String.format("\" La valeur doit être comprise entre %s et %s.\"", minValue, maxValue);
controls.add(0, createFormatControl(controlIdPrefix+"-borne-inf-sup", controlExpression, controlErrorMessage));
controls.addFirst(createFormatControl(controlIdPrefix+"-borne-inf-sup", controlExpression, controlErrorMessage));
}

if(min == null && max != null) {
String maxValue = formatDoubleValue(max, decimalsCount);
String controlExpression = String.format("not(not(isnull(%s)) and %s<%s)", responseName, maxValue, responseName);
String controlErrorMessage = String.format("\" La valeur doit être inférieure à %s.\"", maxValue);
controls.add(0, createFormatControl(controlIdPrefix+"-borne-sup", controlExpression, controlErrorMessage));
controls.addFirst(createFormatControl(controlIdPrefix+"-borne-sup", controlExpression, controlErrorMessage));
}

if(min != null && max == null) {
String minValue = formatDoubleValue(min, decimalsCount);
String controlExpression = String.format("not(not(isnull(%s)) and %s>%s)", responseName, minValue, responseName);
String controlErrorMessage = String.format("\" La valeur doit être supérieure à %s.\"", minValue);
controls.add(0, createFormatControl(controlIdPrefix+"-borne-inf", controlExpression, controlErrorMessage));
controls.addFirst(createFormatControl(controlIdPrefix+"-borne-inf", controlExpression, controlErrorMessage));
}

controls.add(createDecimalsFormatControl(controlIdPrefix, responseName, decimalsCount));
Expand All @@ -148,9 +150,12 @@ private void createFormatControlsForDatepicker(Datepicker datepicker) {

List<ControlType> controls = datepicker.getControls();

Optional<ControlType> control = getFormatControlFromDatepickerAttributes(id, minValue, maxValue, format, responseName);

control.ifPresent(controlType -> controls.add(0, controlType));
Optional<ControlType> controlYearFormat = generateDatepickerYearControl(id, format, responseName);
Optional<ControlType> controlBounds = getFormatControlFromDatepickerAttributes(id, minValue, maxValue, format, responseName);
controlBounds.ifPresent(controls::addFirst);
controlYearFormat.ifPresent(controls::addFirst);
// Note: it's important that the year format is added in first position, since in some cases only the message
// of the first control is displayed.
}

/**
Expand Down Expand Up @@ -188,6 +193,20 @@ private Optional<ControlType> getFormatControlFromDatepickerAttributes(String id
}
return Optional.empty();
}
private Optional<ControlType> generateDatepickerYearControl(String id, String format, String responseName) {
if (format == null || !format.contains("YYYY")) {
log.warn("Datepicker '{}' (id={}) format is {} which doesn't have the year (YYYY).",
responseName, id, format);
return Optional.empty();
}
String controlId = id + "-format-year";
String expression = String.format("not(not(isnull(%s)) and (" +
"cast(cast(cast(%s, date, \"%s\"), string, \"YYYY\"), integer) <= 999 or " +
"cast(cast(cast(%s, date, \"%s\"), string, \"YYYY\"), integer) > 9999))",
responseName, responseName, format, responseName, format);
String message = "\"L'année doit être saisie avec 4 chiffres.\"";
return Optional.of(createFormatControl(controlId, expression, message));
}

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import fr.insee.lunatic.model.flat.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
Expand All @@ -25,13 +26,13 @@ void init() {
datePicker = new Datepicker();
datePicker.setComponentType(ComponentTypeEnum.DATEPICKER);
datePicker.setId("datepicker-id");
datePicker.setResponse(buildResponse("DATEVAR"));
datePicker.setResponse(buildResponse("DATE_VAR"));

number = new InputNumber();
number.setComponentType(ComponentTypeEnum.INPUT_NUMBER);
number.setId("number-id");
number.setDecimals(BigInteger.ZERO);
number.setResponse(buildResponse("NUMBERVAR"));
number.setResponse(buildResponse("NUMBER_VAR"));
}

@Test
Expand All @@ -45,9 +46,9 @@ void shouldInputNumberHaveDecimalsControl() {

List<ControlType> controls = number.getControls();
assertEquals(1, controls.size());
ControlType control = controls.get(0);
ControlType control = controls.getFirst();

assertEquals("not(not(isnull(NUMBERVAR)) and round(NUMBERVAR,10)<>NUMBERVAR)", control.getControl().getValue());
assertEquals("not(not(isnull(NUMBER_VAR)) and round(NUMBER_VAR,10)<>NUMBER_VAR)", control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("number-id-format-decimal", control.getId());
Expand All @@ -68,8 +69,8 @@ void shouldInputNumberHaveScaleOnDecimalValuesWhenDecimalsIsSet() {

List<ControlType> controls = number.getControls();

ControlType control = controls.get(0);
assertEquals("not(not(isnull(NUMBERVAR)) and (5.2400000000>NUMBERVAR or 10.1200000000<NUMBERVAR))", control.getControl().getValue());
ControlType control = controls.getFirst();
assertEquals("not(not(isnull(NUMBER_VAR)) and (5.2400000000>NUMBER_VAR or 10.1200000000<NUMBER_VAR))", control.getControl().getValue());
}

@Test
Expand All @@ -93,9 +94,9 @@ void shouldInputNumberHaveMinMaxFormatControl() {

List<ControlType> controls = number.getControls();
assertEquals(2, controls.size());
ControlType control = controls.get(0);
ControlType control = controls.getFirst();

assertEquals("not(not(isnull(NUMBERVAR)) and (5>NUMBERVAR or 10<NUMBERVAR))", control.getControl().getValue());
assertEquals("not(not(isnull(NUMBER_VAR)) and (5>NUMBER_VAR or 10<NUMBER_VAR))", control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("number-id-format-borne-inf-sup", control.getId());
Expand All @@ -112,9 +113,9 @@ void shouldInputNumberHaveMinFormatControl() {

List<ControlType> controls = number.getControls();
assertEquals(2, controls.size());
ControlType control = controls.get(0);
ControlType control = controls.getFirst();

assertEquals("not(not(isnull(NUMBERVAR)) and 5>NUMBERVAR)", control.getControl().getValue());
assertEquals("not(not(isnull(NUMBER_VAR)) and 5>NUMBER_VAR)", control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("number-id-format-borne-inf", control.getId());
Expand All @@ -131,9 +132,9 @@ void shouldInputNumberHaveMaxFormatControl() {

List<ControlType> controls = number.getControls();
assertEquals(2, controls.size());
ControlType control = controls.get(0);
ControlType control = controls.getFirst();

assertEquals("not(not(isnull(NUMBERVAR)) and 10<NUMBERVAR)", control.getControl().getValue());
assertEquals("not(not(isnull(NUMBER_VAR)) and 10<NUMBER_VAR)", control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("number-id-format-borne-sup", control.getId());
Expand All @@ -142,15 +143,41 @@ void shouldInputNumberHaveMaxFormatControl() {
}

@Test
void shouldDatepickerNotHaveControlsWhenMinMaxNotSet() {
@DisplayName("Datepicker: no format nor min/max, should have no controls")
void datepickerNoFormatNorMinMax() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
processing.apply(lunaticQuestionnaire);
assertTrue(datePicker.getControls() == null || datePicker.getControls().isEmpty());
}

@Test
void shouldDatepickerHaveMinMaxFormatControl() {
@DisplayName("Datepicker: year format control only")
void datepickerYearFormatControl() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
datePicker.setDateFormat("YYYY-MM-DD");

processing.apply(lunaticQuestionnaire);

List<ControlType> controls = datePicker.getControls();
assertEquals(1, controls.size());
ControlType yearControl = controls.getFirst();

assertEquals("datepicker-id-format-year", yearControl.getId());
String expected = "not(not(isnull(DATE_VAR)) and (" +
"cast(cast(cast(DATE_VAR, date, \"YYYY-MM-DD\"), string, \"YYYY\"), integer) <= 999 or " +
"cast(cast(cast(DATE_VAR, date, \"YYYY-MM-DD\"), string, \"YYYY\"), integer) > 9999))";
assertEquals(expected, yearControl.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, yearControl.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, yearControl.getErrorMessage().getType());
assertEquals(ControlTypeEnum.FORMAT, yearControl.getTypeOfControl());
assertEquals(ControlCriticalityEnum.ERROR, yearControl.getCriticality());
}

@Test
@DisplayName("Datepicker: min and max format control")
void datepickerMinMaxFormatControl() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
datePicker.setMin("2020-01-01");
Expand All @@ -159,55 +186,82 @@ void shouldDatepickerHaveMinMaxFormatControl() {
processing.apply(lunaticQuestionnaire);

List<ControlType> controls = datePicker.getControls();
assertEquals(1, controls.size());
ControlType control = controls.get(0);
assertEquals(2, controls.size());
ControlType control = controls.get(1);

assertEquals("not(not(isnull(DATEVAR)) and (cast(DATEVAR, date, \"YYYY-MM-DD\")<cast(\"2020-01-01\", date, \"YYYY-MM-DD\") or cast(DATEVAR, date, \"YYYY-MM-DD\")>cast(\"2023-01-01\", date, \"YYYY-MM-DD\")))", control.getControl().getValue());
assertEquals("datepicker-id-format-date-borne-inf-sup", control.getId());
String expected = "not(not(isnull(DATE_VAR)) and " +
"(cast(DATE_VAR, date, \"YYYY-MM-DD\")<cast(\"2020-01-01\", date, \"YYYY-MM-DD\") or " +
"cast(DATE_VAR, date, \"YYYY-MM-DD\")>cast(\"2023-01-01\", date, \"YYYY-MM-DD\")))";
assertEquals(expected, control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("datepicker-id-format-date-borne-inf-sup", control.getId());
assertEquals(ControlTypeEnum.FORMAT, control.getTypeOfControl());
assertEquals(ControlCriticalityEnum.ERROR, control.getCriticality());
}

@Test
void shouldDatepickerHaveMinFormatControl() {
@DisplayName("Datepicker: min format control")
void datepickerMinFormatControl() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
datePicker.setMin("2020-01-01");
datePicker.setDateFormat("YYYY-MM-DD");
processing.apply(lunaticQuestionnaire);

List<ControlType> controls = datePicker.getControls();
assertEquals(1, controls.size());
ControlType control = controls.get(0);
assertEquals(2, controls.size());
ControlType control = controls.get(1);

assertEquals("not(not(isnull(DATEVAR)) and (cast(DATEVAR, date, \"YYYY-MM-DD\")<cast(\"2020-01-01\", date, \"YYYY-MM-DD\")))", control.getControl().getValue());
assertEquals("datepicker-id-format-date-borne-inf", control.getId());
String expected = "not(not(isnull(DATE_VAR)) and " +
"(cast(DATE_VAR, date, \"YYYY-MM-DD\")<cast(\"2020-01-01\", date, \"YYYY-MM-DD\")))";
assertEquals(expected, control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("datepicker-id-format-date-borne-inf", control.getId());
assertEquals(ControlTypeEnum.FORMAT, control.getTypeOfControl());
assertEquals(ControlCriticalityEnum.ERROR, control.getCriticality());
}

@Test
void shouldDatepickerNumberHaveMaxFormatControl() {
@DisplayName("Datepicker: max format control")
void datepickerMaxFormatControl() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
datePicker.setMax("2023-01-01");
datePicker.setDateFormat("YYYY-MM-DD");
processing.apply(lunaticQuestionnaire);

List<ControlType> controls = datePicker.getControls();
assertEquals(1, controls.size());
ControlType control = controls.get(0);
assertEquals(2, controls.size());
ControlType boundsControl = controls.get(1);

assertEquals("datepicker-id-format-date-borne-sup", boundsControl.getId());
String expected = "not(not(isnull(DATE_VAR)) " +
"and (cast(DATE_VAR, date, \"YYYY-MM-DD\")>cast(\"2023-01-01\", date, \"YYYY-MM-DD\")))";
assertEquals(expected, boundsControl.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, boundsControl.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, boundsControl.getErrorMessage().getType());
assertEquals(ControlTypeEnum.FORMAT, boundsControl.getTypeOfControl());
assertEquals(ControlCriticalityEnum.ERROR, boundsControl.getCriticality());
}

assertEquals("not(not(isnull(DATEVAR)) and (cast(DATEVAR, date, \"YYYY-MM-DD\")>cast(\"2023-01-01\", date, \"YYYY-MM-DD\")))", control.getControl().getValue());
assertEquals(LabelTypeEnum.VTL, control.getControl().getType());
assertEquals(LabelTypeEnum.VTL_MD, control.getErrorMessage().getType());
assertEquals("datepicker-id-format-date-borne-sup", control.getId());
assertEquals(ControlTypeEnum.FORMAT, control.getTypeOfControl());
assertEquals(ControlCriticalityEnum.ERROR, control.getCriticality());
@Test
@DisplayName("Datepicker: year format control when min/max is set")
void datepickerYearAndMinMaxFormatControl() {
lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(datePicker);
datePicker.setMax("2023-01-01");
datePicker.setDateFormat("YYYY-MM-DD");
processing.apply(lunaticQuestionnaire);

List<ControlType> controls = datePicker.getControls();
assertEquals(2, controls.size());
ControlType yearControl = controls.get(0);
ControlType boundsControl = controls.get(1);
// Only testing ids here to check that the order is right, content is tested in other tests
assertEquals("datepicker-id-format-year", yearControl.getId());
assertEquals("datepicker-id-format-date-borne-sup", boundsControl.getId());
}

@Test
Expand All @@ -226,7 +280,7 @@ void shouldLoopComponentsBeProcessed() {

processing.apply(lunaticQuestionnaire);

assertEquals(1, datePicker.getControls().size());
assertEquals(2, datePicker.getControls().size());
assertEquals(2, number.getControls().size());
}

Expand All @@ -240,12 +294,12 @@ void shouldTableComponentsHaveSubComponentsControls() {

List<BodyCell> bodyCells = new ArrayList<>();
bodyCells.add(buildBodyCell("line1"));
bodyCells.add(buildBodyCell(table.getId()+"-co", "CHECKVAR", ComponentTypeEnum.CHECKBOX_ONE));
bodyCells.add(buildBodyCell(table.getId()+"-co", "CHECKBOX_VAR", ComponentTypeEnum.CHECKBOX_ONE));
bodyLines.add(buildBodyLine(bodyCells));

bodyCells = new ArrayList<>();
bodyCells.add(buildBodyCell("line2"));
bodyCells.add(buildBodyCell(table.getId()+"-number", "NUMBERVAR", ComponentTypeEnum.INPUT_NUMBER, BigInteger.TWO, 2.0, 5.0));
bodyCells.add(buildBodyCell(table.getId()+"-number", "NUMBER_VAR", ComponentTypeEnum.INPUT_NUMBER, BigInteger.TWO, 2.0, 5.0));
bodyLines.add(buildBodyLine(bodyCells));

lunaticQuestionnaire = new Questionnaire();
Expand All @@ -266,8 +320,8 @@ void shouldRosterComponentsHaveSubComponentsControls() {

List<BodyCell> bodyCells = roster.getComponents();
bodyCells.add(buildBodyCell("line1"));
bodyCells.add(buildBodyCell(roster.getId()+"-number", "NUMBERVAR", ComponentTypeEnum.INPUT_NUMBER, BigInteger.TWO, 2.0, 5.0));
bodyCells.add(buildBodyCell(roster.getId()+"-co", "CHECKVAR", ComponentTypeEnum.CHECKBOX_ONE));
bodyCells.add(buildBodyCell(roster.getId()+"-number", "NUMBER_VAR", ComponentTypeEnum.INPUT_NUMBER, BigInteger.TWO, 2.0, 5.0));
bodyCells.add(buildBodyCell(roster.getId()+"-co", "CHECKBOX_VAR", ComponentTypeEnum.CHECKBOX_ONE));

lunaticQuestionnaire = new Questionnaire();
lunaticQuestionnaire.getComponents().add(roster);
Expand Down

0 comments on commit 6cc98e1

Please sign in to comment.