Commit fe61a990 authored by PECQUOT's avatar PECQUOT
Browse files

[enh] Add duplication on surveys ungrouped measurements (Mantis #47249)


[enh] Create sampling operations wizard can handle a specified label prefix (Mantis #47245)
[enh] Use a alphanumeric comparator to handle better ergonomic sort of labels in tables (Mantis #47377)
[enh] Add pmfmId column (hidden by default) in programs, rules, filters screens (Mantis #46383)
Signed-off-by: PECQUOT's avatarlp1ee9d <ludovic.pecquot@e-is.pro>
parent 00a9bf95
......@@ -189,7 +189,6 @@
<!-- Last ReefDb db and plugin version -->
<dbVersion>2015.03.27-2</dbVersion>
<hibernate.version>4.3.9.Final</hibernate.version>
<hibernatespatial.version>4.3-20140213</hibernatespatial.version>
<hibernate-tools.version>4.3.4.Final</hibernate-tools.version>
......@@ -221,10 +220,13 @@
<junit.version>4.12</junit.version>
<!-- release config -->
<autoVersionSubmodules>true</autoVersionSubmodules>
<goals>deploy</goals>
<arguments>-Dliquibase.should.run=false</arguments>
<preparationGoals>verify</preparationGoals>
<!-- <autoVersionSubmodules>true</autoVersionSubmodules>-->
<!-- <goals>deploy</goals>-->
<!-- <arguments>-Dliquibase.should.run=false</arguments>-->
<!-- <preparationGoals>verify</preparationGoals>-->
<!-- gitflow release plugin -->
<gitflowPluginVersion>1.12.0</gitflowPluginVersion>
<!--license-maven-plugin config -->
<maven.license.file>${project.basedir}/LICENSE.txt</maven.license.file>
......@@ -374,6 +376,17 @@
<version>3.6.0.1398</version>
</plugin>
<plugin>
<groupId>com.amashchenko.maven.plugin</groupId>
<artifactId>gitflow-maven-plugin</artifactId>
<version>${gitflowPluginVersion}</version>
<configuration>
<!-- optional configuration -->
<verbose>true</verbose>
<skipTestProject>true</skipTestProject>
</configuration>
</plugin>
</plugins>
<pluginManagement>
......
......@@ -45,6 +45,7 @@ public interface DecoratorService extends fr.ifremer.quadrige3.core.service.deco
/** Constant <code>NAME_WITH_UNIT="nameWithUnit"</code> */
String NAME_WITH_UNIT = "nameWithUnit";
String NAME_WITH_ID = "nameWithId";
/** Constant <code>DESCRIPTION="description"</code> */
String DESCRIPTION = "description";
......
......@@ -91,6 +91,7 @@ public class DecoratorServiceImpl extends fr.ifremer.quadrige3.core.service.deco
provider.registerDecorator(FOR_EXTRACTION, new PMFMExtractionDecorator());
provider.registerDecorator(NAME, new PMFMNameDecorator());
provider.registerDecorator(NAME_WITH_UNIT, new PMFMNameWithUnitDecorator());
provider.registerDecorator(NAME_WITH_ID, new PMFMNameWithIdDecorator());
provider.registerDecorator(UnitDTO.class, WITH_SYMBOL, "${name}$s (${symbol}$s)", TOKEN_SEPARATOR, SEPARATOR);
provider.registerDecorator(new TaxonDecorator());
provider.registerDecorator(TaxonDTO.class, CODE, "${id}$s", TOKEN_SEPARATOR, SEPARATOR); // TODO: where is it used ?
......@@ -367,6 +368,14 @@ public class DecoratorServiceImpl extends fr.ifremer.quadrige3.core.service.deco
}
}
private class PMFMNameWithIdDecorator extends PMFMNameDecorator {
PMFMNameWithIdDecorator() {
// The id is put at the end of the string to preserve alphabetic order (Mantis #46672)
super("${" + TOKEN_NAME + "}$s#${parameter/code}$s#${matrix/name}$s#${fraction/name}$s#${method/name}$s#${unit/symbol}$s#${id}$s");
}
}
/**
* Survey decorator.
*/
......
......@@ -12,12 +12,12 @@ package fr.ifremer.reefdb.dto;
* 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 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 <http://www.gnu.org/licenses/>.
* #L%
......@@ -61,15 +61,12 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Array;
import java.sql.SQLException;
import java.text.Collator;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.nuiton.i18n.I18n.t;
......@@ -212,34 +209,6 @@ public class ReefDbBeans extends Beans {
return decimalFormat;
}
/**
* <p>roundDouble.</p>
*
* @param number a {@link Double} object.
* @return a {@link Double} object.
*/
public static Double roundDouble(Double number) {
return roundDouble(number, 2);
}
/**
* <p>roundDouble.</p>
*
* @param number a {@link Double} object.
* @param nbDecimal a int.
* @return a {@link Double} object.
*/
public static Double roundDouble(Double number, int nbDecimal) {
if (number == null) {
return null;
}
if (nbDecimal == 0) {
return (double) Math.round(number);
}
double oper = Math.pow(10, nbDecimal);
return Math.round(number * oper) / oper;
}
/**
* <p>computeAddress.</p>
*
......@@ -1030,7 +999,7 @@ public class ReefDbBeans extends Beans {
/**
* <p>addUniqueErrors.</p>
*
* @param bean a {@link ErrorAware} object.
* @param bean a {@link ErrorAware} object.
* @param errorsToAdd a {@link Collection} object.
*/
public static void addUniqueErrors(ErrorAware bean, Collection<ErrorDTO> errorsToAdd) {
......@@ -1058,31 +1027,6 @@ public class ReefDbBeans extends Beans {
&& Objects.equals(error.getMessage(), thisError.getMessage());
}
/**
* Fill both list with absent elements from each other. Keep insertion order safe.
*
* @param list1 a {@link List} object.
* @param list2 a {@link List} object.
*/
public static <B extends ReefDbBean> void fillListsEachOther(List<B> list1, List<B> list2) {
if (list1 == null || list2 == null) {
return;
}
for (B bean : list1) {
if (!list2.contains(bean)) {
list2.add(bean);
}
}
for (B bean : list2) {
if (!list1.contains(bean)) {
list1.add(bean);
}
}
}
/**
* <p>isPmfmMandatory.</p>
*
......@@ -1183,77 +1127,4 @@ public class ReefDbBeans extends Beans {
return null;
}
/**
* Compares two alphanumeric codes by first check a common prefix and compare following numeric values, if any
* else compare strings directly if no numeric part found
*
* @param string1 the first string
* @param string2 the second string
* @return compare result
*/
public static int compareIntegerFromPrefixedString(String string1, String string2) {
if (StringUtils.isNoneBlank(string1, string2)) {
// find common prefix
String prefix = getCommonPrefix(string1, string2);
// remove it and remove all non-digit characters
String value1 = getCleanedAlphaNumericString(StringUtils.removeStartIgnoreCase(string1, prefix));
String value2 = getCleanedAlphaNumericString(StringUtils.removeStartIgnoreCase(string2, prefix));
if (StringUtils.isNumeric(value1) && StringUtils.isNumeric(value2)) {
Integer i1 = Integer.valueOf(value1);
Integer i2 = Integer.valueOf(value2);
return i1.compareTo(i2);
} else {
// return default string comparison
return Collator.getInstance().compare(value1, value2);
}
}
// return default string comparison
return Collator.getInstance().compare(string1, string2);
}
/**
* return the common prefix shared by both input strings
*
* @param string1 the first string
* @param string2 the second string
* @return the common prefix
*/
public static String getCommonPrefix(String string1, String string2) {
String prefix = "";
if (StringUtils.isNoneBlank(string1, string2)) {
int length = Math.min(string1.length(), string2.length());
for (int i = 0; i < length; i++) {
String p1 = string1.substring(i, i + 1);
String p2 = string2.substring(i, i + 1);
if (!StringUtils.isNumeric(p1) && !StringUtils.isNumeric(p2) && p1.equalsIgnoreCase(p2)) {
prefix += p1;
} else break;
}
}
return prefix;
}
/**
* return a cleaned alphanumeric code by removing all characters after a valid code
* like "SZ1" if input is "SZ1 - name"
* or "1" if input is "1_some_extra_string"
*
* @param string the string
* @return the cleaned string
*/
public static String getCleanedAlphaNumericString(String string) {
if (string == null) return null;
Matcher matcher = Pattern.compile("^\\D*\\d*").matcher(string);
if (matcher.find()) {
return matcher.group(0);
}
return string;
}
}
......@@ -121,10 +121,11 @@ public interface ObservationService {
* @param survey survey to duplicate
* @param fullDuplication enable full duplication
* @param copyCoordinates enable copy coordinates
* @param duplicateSurveyMeasurements enable copy survey ungrouped measurements
* @return the duplicated survey
*/
@PreAuthorize("hasPermission(null, T(fr.ifremer.quadrige3.core.security.QuadrigeUserAuthority).USER)")
SurveyDTO duplicateSurvey(SurveyDTO survey, boolean fullDuplication, boolean copyCoordinates);
SurveyDTO duplicateSurvey(SurveyDTO survey, boolean fullDuplication, boolean copyCoordinates, boolean duplicateSurveyMeasurements);
/**
* Duplicate prelevement.
......
......@@ -423,10 +423,12 @@ public class ObservationServiceImpl implements ObservationInternalService {
* {@inheritDoc}
*/
@Override
public SurveyDTO duplicateSurvey(SurveyDTO survey, boolean fullDuplication, boolean copyCoordinates) {
public SurveyDTO duplicateSurvey(SurveyDTO survey, boolean fullDuplication, boolean copyCoordinates, boolean duplicateSurveyMeasurements) {
if (survey == null) return null;
// Duplicate survey
SurveyDTO duplicateSurvey = ReefDbBeans.clone(survey);
Assert.notNull(duplicateSurvey);
// Remove ID
duplicateSurvey.setId(null);
......@@ -473,6 +475,17 @@ public class ObservationServiceImpl implements ObservationInternalService {
duplicateSurvey.getOccasion().setId(null);
}
// Copy survey ungrouped (= non individual) measurements (Mantis #47249)
if (duplicateSurveyMeasurements && survey.getId() != null) {
List<MeasurementDTO> measurements = measurementDao.getMeasurementsBySurveyId(survey.getId(), config.getDepthValuesPmfmId());
duplicateSurvey.setMeasurements(measurements.stream().filter(measurement -> measurement.getIndividualId() == null).collect(Collectors.toList()));
duplicateSurvey.getMeasurements().forEach(measurement -> measurement.setId(null));
String comment = t("reefdb.service.observation.duplicate.measurementComment", LocalDate.now(), survey.getName());
duplicateSurvey.getMeasurements().forEach(measurement -> measurement.setComment(comment));
duplicateSurvey.setPmfms(ReefDbBeans.clone(survey.getPmfms())); // utile ?
duplicateSurvey.setMeasurementsLoaded(true);
}
if (fullDuplication) {
// ensure sampling operations are loaded
......@@ -483,6 +496,7 @@ public class ObservationServiceImpl implements ObservationInternalService {
if (!survey.isSamplingOperationsEmpty()) {
for (SamplingOperationDTO samplingOperation : survey.getSamplingOperations()) {
SamplingOperationDTO duplicateSamplingOperation = ReefDbBeans.clone(samplingOperation);
Assert.notNull(duplicateSamplingOperation);
duplicateSamplingOperation.setId(null);
duplicateSamplingOperation.setMeasurements(null);
duplicateSamplingOperation.setIndividualMeasurements(null);
......
......@@ -369,6 +369,7 @@ reefdb.service.extraction.fieldNamePrefix.SAMPLING_OPER=
reefdb.service.extraction.fieldNamePrefix.SURVEY=
reefdb.service.extraction.noData.error=
reefdb.service.extraction.noPmfm.error=
reefdb.service.observation.duplicate.measurementComment=
reefdb.service.observation.unvalidation.progression=
reefdb.service.observation.validation.progression=
reefdb.service.persistence.checkArchiveDb.error=
......
......@@ -426,6 +426,7 @@ reefdb.service.extraction.fieldNamePrefix.SAMPLING_OPER=REPLICAT
reefdb.service.extraction.fieldNamePrefix.SURVEY=OBSERVATION
reefdb.service.extraction.noData.error=Aucune observation ne correspond aux critères d'extraction.
reefdb.service.extraction.noPmfm.error=Aucune mesure ne correspond aux critères d'extraction.
reefdb.service.observation.duplicate.measurementComment=Résultat dupliqué le %s depuis l'observation %s
reefdb.service.observation.save=Sauvegarde
reefdb.service.observation.unvalidation.progression=Dévalidation de %s observation(s) [%s-%s]
reefdb.service.observation.validation.progression=Validation de %s observation(s) [%s-%s]
......
......@@ -628,7 +628,7 @@
<forge.file2Processor>Linux 64-bit</forge.file2Processor>
<forge.file3Type>text</forge.file3Type>
<forge.file3>${project.build.directory}/../../src/changes/RELEASE_NOTES.txt</forge.file3>
<forge.file3>${project.build.directory}/../../CHANGELOG.md</forge.file3>
<forge.file3Processor>Any</forge.file3Processor>
</properties>
......
......@@ -24,12 +24,8 @@ package fr.ifremer.reefdb.ui.swing.content.home.operation;
*/
import fr.ifremer.reefdb.dto.data.sampling.SamplingOperationDTO;
import fr.ifremer.reefdb.dto.referential.pmfm.PmfmDTO;
import fr.ifremer.reefdb.ui.swing.content.home.survey.SurveysTableRowModel;
import fr.ifremer.reefdb.ui.swing.util.table.AbstractReefDbTableUIModel;
import fr.ifremer.reefdb.ui.swing.util.table.PmfmTableColumn;
import java.util.List;
/**
* @author peck7 on 28/06/2018.
......@@ -41,51 +37,6 @@ public abstract class AbstractOperationsTableUIModel<M extends AbstractOperation
public static final String PROPERTY_SURVEY = "survey";
public static final String PROPERTY_SURVEY_EDITABLE = "surveyEditable";
/**
* Property pour les colonnes dynamiques.
*/
private List<PmfmDTO> pmfms;
private List<PmfmTableColumn> pmfmColumns;
public static final String PROPERTY_PMFM_COLUMNS = "pmfmColumns";
/**
* <p>Getter for the field <code>pmfms</code>.</p>
*
* @return a {@link List} object.
*/
public List<PmfmDTO> getPmfms() {
return pmfms;
}
/**
* <p>Setter for the field <code>pmfms</code>.</p>
*
* @param pmfms a {@link List} object.
*/
public void setPmfms(List<PmfmDTO> pmfms) {
this.pmfms = pmfms;
}
/**
* <p>Getter for the field <code>pmfmColumns</code>.</p>
*
* @return a {@link List} object.
*/
public List<PmfmTableColumn> getPmfmColumns() {
return pmfmColumns;
}
/**
* <p>Setter for the field <code>pmfmColumns</code>.</p>
*
* @param pmfmColumns a {@link List} object.
*/
public void setPmfmColumns(List<PmfmTableColumn> pmfmColumns) {
this.pmfmColumns = pmfmColumns;
firePropertyChange(PROPERTY_PMFM_COLUMNS, null, pmfmColumns);
}
/**
* <p>Getter for the field <code>survey</code>.</p>
*
......
......@@ -23,6 +23,7 @@ package fr.ifremer.reefdb.ui.swing.content.home.operation;
* #L%
*/
import fr.ifremer.quadrige3.core.dao.technical.AlphanumericComparator;
import fr.ifremer.quadrige3.core.dao.technical.StringIterator;
import fr.ifremer.quadrige3.ui.swing.common.action.ActionFactory;
import fr.ifremer.quadrige3.ui.swing.common.component.coordinate.CoordinateEditor;
......@@ -46,7 +47,6 @@ import fr.ifremer.reefdb.ui.swing.content.home.survey.SurveysTableRowModel;
import fr.ifremer.reefdb.ui.swing.util.AbstractReefDbBeanUIModel;
import fr.ifremer.reefdb.ui.swing.util.ReefDbUI;
import fr.ifremer.reefdb.ui.swing.util.table.AbstractReefDbTableUIHandler;
import fr.ifremer.reefdb.ui.swing.util.table.PmfmTableColumn;
import fr.ifremer.reefdb.ui.swing.util.table.comment.CommentCellEditor;
import fr.ifremer.reefdb.ui.swing.util.table.comment.CommentCellRenderer;
import jaxx.runtime.SwingUtil;
......@@ -266,28 +266,13 @@ public class OperationsTableUIHandler extends AbstractReefDbTableUIHandler<Opera
}
private void populateDynamicColumns() {
// First, remove old dynamic columns
removeColumns(getModel().getPmfmColumns());
List<PmfmTableColumn> dynamicColumns = addPmfmColumns(
getModel().getPmfms(),
SamplingOperationDTO.PROPERTY_PMFMS,
DecoratorService.NAME_WITH_UNIT,
OperationsTableModel.TIME);
getModel().setPmfmColumns(dynamicColumns);
}
/**
* Initialisation de la table prelevements.
*/
private void iniTable() {
// Colonne mnemonique
final TableColumnExt colName = addColumn(
OperationsTableModel.NAME);
final TableColumnExt colName = addColumn(OperationsTableModel.NAME);
colName.setSortable(true);
colName.setMinWidth(100);
......@@ -430,6 +415,16 @@ public class OperationsTableUIHandler extends AbstractReefDbTableUIHandler<Opera
getTable().setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, getConfig().getColorEditionPanelBorder()));
}
@Override
protected void installSortController() {
super.installSortController();
getSortController().setComparator(
getTable().getColumnModel().getColumnExt(OperationsTableModel.NAME).getModelIndex(),
new AlphanumericComparator()
);
}
private void createSamplingEquipmentCellEditor() {
samplingEquipmentCellEditor = newExtendedComboBoxCellEditor(null, SamplingEquipmentDTO.class, false);
......@@ -537,8 +532,9 @@ public class OperationsTableUIHandler extends AbstractReefDbTableUIHandler<Opera
List<OperationsTableRowModel> newOperations = dialog.getModel().getRows();
if (dialog.getModel().isValid() && CollectionUtils.isNotEmpty(newOperations)) {
// Use label auto generation
StringIterator labelIterator = StringIterator.newStringIteratorByProperty(getModel().getRows(), SamplingOperationDTO.PROPERTY_NAME);
// Use label auto generation, now with specified label prefix (Mantis #47245)
String labelPrefix = dialog.getModel().getOperationNamePrefix();
StringIterator labelIterator = StringIterator.newStringIteratorByProperty(getModel().getRows(), SamplingOperationDTO.PROPERTY_NAME, labelPrefix);
newOperations.forEach(newOperation -> newOperation.setName(labelIterator.next()));
getModel().addRows(newOperations);
......@@ -719,7 +715,12 @@ public class OperationsTableUIHandler extends AbstractReefDbTableUIHandler<Opera
getModel().setPmfms(pmfms);
// Initialiser la table des prelevements
populateDynamicColumns();
addPmfmColumns(
getModel().getPmfms(),
SamplingOperationDTO.PROPERTY_PMFMS,
DecoratorService.NAME_WITH_UNIT,
OperationsTableModel.TIME);
boolean notEmpty = CollectionUtils.isNotEmpty(getModel().getPmfmColumns());
if (survey != null && !survey.isSamplingOperationsEmpty()) {
......
......@@ -45,6 +45,8 @@
<JPanel layout="{new FlowLayout()}" constraints='BorderLayout.PAGE_START'>
<JLabel id="nbOperationLabel"/>
<NumberEditor id="nbOperationEditor" constructorParams='this'/>
<JLabel id="operationNamePrefixLabel"/>
<JTextField id="operationNamePrefixEditor" onKeyReleased='handler.setText(event, "operationNamePrefix")'/>
</JPanel>
<JScrollPane id="scrollPane" constraints='BorderLayout.CENTER'>
......
......@@ -46,6 +46,16 @@
_selectOnFocus: {true};
}
#operationNamePrefixLabel {
text: "reefdb.home.samplingOperation.new.operationNamePrefix.label";
labelFor: {operationNamePrefixEditor};
}
#operationNamePrefixEditor {
text: {model.getOperationNamePrefix()};
_selectOnFocus: {true};
}
#cancelButton {
actionIcon: cancel;
text: "reefdb.common.cancel";
......
......@@ -42,7 +42,6 @@ import fr.ifremer.reefdb.ui.swing.content.home.operation.OperationsTableUIModel;
import fr.ifremer.reefdb.ui.swing.util.ReefDbUI;
import fr.ifremer.reefdb.ui.swing.util.table.AbstractReefDbTableModel;
import fr.ifremer.reefdb.ui.swing.util.table.AbstractReefDbTableUIHandler;
import fr.ifremer.reefdb.ui.swing.util.table.PmfmTableColumn;
import org.apache.commons.collections4.CollectionUtils;
import org.jdesktop.swingx.table.TableColumnExt;
import org.nuiton.jaxx.application.swing.util.Cancelable;
......@@ -104,7 +103,7 @@ public class AddOperationTableUIHandler extends AbstractReefDbTableUIHandler<Ope
// add listener on TAB key to follow focus to table
String tabActionName = "newTABAction";
JTextField field = getUI().getNbOperationEditor().getTextField();
JTextField field = getUI().getOperationNamePrefixEditor();
field.setFocusTraversalKeysEnabled(false);
field.getInputMap().put(KeyStroke.getKeyStroke("pressed TAB"), tabActionName);
field.getActionMap().put(tabActionName, new AbstractAction() {
......@@ -127,12 +126,22 @@ public class AddOperationTableUIHandler extends AbstractReefDbTableUIHandler<Ope
// Chargement des psfms du tableau
List<PmfmDTO> pmfms = tableModel.getPmfms();
getModel().setPmfms(pmfms);
populateDynamicColumns();
addPmfmColumns(
getModel().getPmfms(),
SamplingOperationDTO.PROPERTY_PMFMS,
DecoratorService.NAME_WITH_UNIT,
OperationsTableModel.TIME);
boolean notEmpty = CollectionUtils.isNotEmpty(getModel().getPmfmColumns());
// Le prelevement
SamplingOperationDTO newSamplingOperation = getContext().getObservationService().newSamplingOperation(getModel().getSurvey(), pmfms);
// init operation name prefix editor
getModel().setOperationNamePrefix(
StringIterator.newStringIteratorByProperty(getModel().getSurvey().getSamplingOperations(), SamplingOperationDTO.PROPERTY_NAME).getPrefix()
);
// Chargement du tableau avec les prelevements
getModel().setRows(null);
OperationsTableRowModel newRow = getModel().addNewRow(newSamplingOperation);
......@@ -149,20 +158,6 @@ public class AddOperationTableUIHandler extends AbstractReefDbTableUIHandler<Ope
}
private void populateDynamicColumns() {
// First, remove old dynamic columns
removeColumns(getModel().getPmfmColumns());
List<PmfmTableColumn> dynamicColumns = addPmfmColumns(
getModel().getPmfms(),
SamplingOperationDTO.PROPERTY_PMFMS,
DecoratorService.NAME_WITH_UNIT,
OperationsTableModel.TIME);
getModel().setPmfmColumns(dynamicColumns);
}
/**