mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 21:20:45 +00:00
Merged dev into feature/CE-881-create-basic-saved-reports
This commit is contained in:
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
@ -32,10 +33,13 @@ import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -210,4 +214,23 @@ class QValueFormatterTest extends BaseTest
|
||||
assertEquals("2023-02-01 07:15:47 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 47), ZoneId.of("US/Central"))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldDisplayBehaviors()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
|
||||
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withZoneIdFromFieldName("timeZone"));
|
||||
|
||||
QRecord record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z")).withValue("timeZone", "America/Chicago");
|
||||
QValueFormatter.setDisplayValuesInRecords(table, List.of(record));
|
||||
assertEquals("2024-04-04 02:12:00 PM CDT", record.getDisplayValue("createDate"));
|
||||
}
|
||||
|
||||
}
|
@ -29,9 +29,12 @@ import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -140,6 +143,36 @@ class ValueBehaviorApplierTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testApplyFormattingBehaviors()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.getField("firstName").withBehavior(ToUpperCaseBehavior.getInstance());
|
||||
table.getField("lastName").withBehavior(ToUpperCaseBehavior.NOOP);
|
||||
table.getField("ssn").withBehavior(ValueTooLongBehavior.TRUNCATE).withMaxLength(1);
|
||||
|
||||
QRecord record = new QRecord().withValue("firstName", "Homer").withValue("lastName", "Simpson").withValue("ssn", "0123456789");
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
|
||||
|
||||
assertEquals("HOMER", record.getDisplayValue("firstName"));
|
||||
assertNull(record.getDisplayValue("lastName")); // noop will literally do nothing, not even pass value through.
|
||||
assertEquals("0123456789", record.getValueString("ssn")); // formatting action should not run the too-long truncate behavior
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now put to-upper-case behavior on lastName, but run INSERT actions - and make sure it doesn't get applied. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
table.getField("lastName").withBehavior(ToUpperCaseBehavior.getInstance());
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
|
||||
assertNull(record.getDisplayValue("lastName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -153,4 +186,73 @@ class ValueBehaviorApplierTest extends BaseTest
|
||||
return (recordOpt.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class ToUpperCaseBehavior implements FieldDisplayBehavior<ToUpperCaseBehavior>
|
||||
{
|
||||
private final boolean enabled;
|
||||
|
||||
private static ToUpperCaseBehavior NOOP = new ToUpperCaseBehavior(false);
|
||||
private static ToUpperCaseBehavior instance = new ToUpperCaseBehavior(true);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private ToUpperCaseBehavior(boolean enabled)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ToUpperCaseBehavior getDefault()
|
||||
{
|
||||
return (NOOP);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ToUpperCaseBehavior getInstance()
|
||||
{
|
||||
return (instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
if(!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for(QRecord record : CollectionUtils.nonNullList(recordList))
|
||||
{
|
||||
String displayValue = record.getValueString(field.getName());
|
||||
if(displayValue != null)
|
||||
{
|
||||
displayValue = displayValue.toUpperCase();
|
||||
}
|
||||
|
||||
record.setDisplayValue(field.getName(), displayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
@ -56,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
@ -1758,6 +1761,26 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldBehaviors()
|
||||
{
|
||||
BiFunction<QInstance, String, QFieldMetaData> fieldExtractor = (QInstance qInstance, String fieldName) -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField(fieldName);
|
||||
assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance, "firstName").withBehaviors(Set.of(ValueTooLongBehavior.ERROR, ValueTooLongBehavior.TRUNCATE)).withMaxLength(1)),
|
||||
"more than 1 fieldBehavior of type ValueTooLongBehavior, which is not allowed");
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// make sure a custom validation method in a field behavior gets applied //
|
||||
// more tests for this particular behavior are in its own test class //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance, "firstName").withBehavior(new DateTimeDisplayValueBehavior())),
|
||||
"DateTimeDisplayValueBehavior was a applied to a non-DATE_TIME field");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.fields;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for DateTimeDisplayValueBehavior
|
||||
*******************************************************************************/
|
||||
class DateTimeDisplayValueBehaviorTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testZoneIdFromFieldName()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
|
||||
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withZoneIdFromFieldName("timeZone"));
|
||||
|
||||
QRecord record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z")).withValue("timeZone", "America/Chicago");
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
|
||||
assertEquals("2024-04-04 02:12:00 PM CDT", record.getDisplayValue("createDate"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testZoneIdFromFieldNameWithFallback()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
|
||||
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withZoneIdFromFieldName("timeZone").withFallbackZoneId("America/Denver"));
|
||||
|
||||
QRecord record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z")).withValue("timeZone", "whodis");
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
|
||||
assertEquals("2024-04-04 01:12:00 PM MDT", record.getDisplayValue("createDate"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDefaultZoneId()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
|
||||
table.getField("createDate").withBehavior(new DateTimeDisplayValueBehavior().withDefaultZoneId("America/Los_Angeles"));
|
||||
|
||||
QRecord record = new QRecord().withValue("createDate", Instant.parse("2024-04-04T19:12:00Z"));
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, qInstance, table, List.of(record), null);
|
||||
assertEquals("2024-04-04 12:12:00 PM PDT", record.getDisplayValue("createDate"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testValidation()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
QFieldMetaData field = table.getField("createDate");
|
||||
table.withField(new QFieldMetaData("timeZone", QFieldType.STRING));
|
||||
|
||||
Function<Consumer<DateTimeDisplayValueBehavior>, List<String>> testOne = setup ->
|
||||
{
|
||||
DateTimeDisplayValueBehavior dateTimeDisplayValueBehavior = new DateTimeDisplayValueBehavior();
|
||||
setup.accept(dateTimeDisplayValueBehavior);
|
||||
return (dateTimeDisplayValueBehavior.validateBehaviorConfiguration(table, field));
|
||||
};
|
||||
|
||||
///////////////////
|
||||
// valid configs //
|
||||
///////////////////
|
||||
assertThat(testOne.apply(b -> b.toString())).isEmpty(); // default setup (noop use-case) is valid
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("timeZone"))).isEmpty();
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("timeZone").withFallbackZoneId("UTC"))).isEmpty();
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("timeZone").withFallbackZoneId("America/Chicago"))).isEmpty();
|
||||
assertThat(testOne.apply(b -> b.withDefaultZoneId("UTC"))).isEmpty();
|
||||
assertThat(testOne.apply(b -> b.withDefaultZoneId("America/Chicago"))).isEmpty();
|
||||
|
||||
/////////////////////
|
||||
// invalid configs //
|
||||
/////////////////////
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("notAField")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("Unrecognized field name");
|
||||
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("id")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("A non-STRING type [INTEGER] was specified as the zoneIdFromFieldName field");
|
||||
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("timeZone").withDefaultZoneId("UTC")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("You may not specify both zoneIdFromFieldName and defaultZoneId");
|
||||
|
||||
assertThat(testOne.apply(b -> b.withDefaultZoneId("UTC").withFallbackZoneId("UTC")))
|
||||
.hasSize(2)
|
||||
.anyMatch(s -> s.contains("You may not specify both defaultZoneId and fallbackZoneId"))
|
||||
.anyMatch(s -> s.contains("You may only set fallbackZoneId if using zoneIdFromFieldName"));
|
||||
|
||||
assertThat(testOne.apply(b -> b.withFallbackZoneId("UTC")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("You may only set fallbackZoneId if using zoneIdFromFieldName");
|
||||
|
||||
assertThat(testOne.apply(b -> b.withDefaultZoneId("notAZone")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("Invalid ZoneId [notAZone] for [defaultZoneId]");
|
||||
|
||||
assertThat(testOne.apply(b -> b.withZoneIdFromFieldName("timeZone").withFallbackZoneId("notAZone")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("Invalid ZoneId [notAZone] for [fallbackZoneId]");
|
||||
|
||||
assertThat(new DateTimeDisplayValueBehavior().validateBehaviorConfiguration(table, table.getField("firstName")))
|
||||
.hasSize(1).first().asString()
|
||||
.contains("non-DATE_TIME field [firstName]");
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.state;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -88,4 +90,42 @@ public class InMemoryStateProviderTest extends BaseTest
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void testClean()
|
||||
{
|
||||
InMemoryStateProvider stateProvider = InMemoryStateProvider.getInstance();
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// Add an entry that is 3 hours old, should not be cleaned //
|
||||
/////////////////////////////////////////////////////////////
|
||||
UUIDAndTypeStateKey newKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.PROCESS_STATUS, Instant.now().minus(3, ChronoUnit.HOURS));
|
||||
String newUUID = UUID.randomUUID().toString();
|
||||
QRecord newQRecord = new QRecord().withValue("uuid", newUUID);
|
||||
stateProvider.put(newKey, newQRecord);
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Add an entry that is 5 hours old, it should be cleaned //
|
||||
////////////////////////////////////////////////////////////
|
||||
UUIDAndTypeStateKey oldKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.PROCESS_STATUS, Instant.now().minus(5, ChronoUnit.HOURS));
|
||||
String oldUUID = UUID.randomUUID().toString();
|
||||
QRecord oldQRecord = new QRecord().withValue("uuid", oldUUID);
|
||||
stateProvider.put(oldKey, oldQRecord);
|
||||
|
||||
///////////////////
|
||||
// Call to clean //
|
||||
///////////////////
|
||||
stateProvider.clean(Instant.now().minus(4, ChronoUnit.HOURS));
|
||||
|
||||
QRecord qRecordFromState = stateProvider.get(QRecord.class, newKey).get();
|
||||
Assertions.assertEquals(newUUID, qRecordFromState.getValueString("uuid"), "Should read value from state persistence");
|
||||
|
||||
Assertions.assertTrue(stateProvider.get(QRecord.class, oldKey).isEmpty(), "Key not found in state should return empty");
|
||||
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user